Introduction
Centinel is a low-code middleware framework that implements the HTTP 402 Payment Required protocol to monetize bot and AI agent traffic on your web server.
Why Centinel?
AI agents, web scrapers, and automated scripts (ChatGPT, ClaudeBot, custom crawlers) now account for a rapidly growing share of internet traffic. Traditional responses are binary: allow them unlimited free access, or block them entirely with CAPTCHAs and WAFs.
Centinel introduces a third way: The Agentic Web Paywall. Instead of blocking bots, Centinel challenges them with a machine-readable payment request. If the agent pays (in USDC, SOL, or ETH), it gets instant, cryptographically verified access. No sign-ups. No credit cards. No subscription overhead.
What Centinel Protects
Centinel sits as middleware on your server and intercepts requests to any route you define — API endpoints, page routes, file paths, or any URL pattern. When an unpaid request hits a protected route, Centinel responds with a standardized 402 Payment Required challenge containing wallet addresses, price, and chain information. The agent parses this, pays on-chain, and retries with the transaction signature.
The x402 Protocol Flow
- Request: An agent or bot sends a request to a protected route.
- Challenge: Centinel intercepts the request and returns HTTP 402 with payment instructions in WWW-Authenticate headers and a JSON body.
- Payment: The agent executes a USDC/SOL/ETH transfer on Solana or Base, then retries the request with the transaction signature in X-Payment-Signature and X-Payment-Chain headers.
- Verification: Centinel verifies the transaction on-chain (destination, amount, recency). On success, it issues a cryptographically signed Time-Locked Proof (JWT) and serves the route payload.
- Session: For per_session routes, subsequent requests within the TTL window bypass payment using the JWT proof.
Supported Payments
Centinel natively verifies transfers of USDC, SOL (native Solana), and ETH (native Base) across both supported blockchains. USDC provides stable, predictable pricing while native token support gives agents maximum flexibility.
Quick Start
Get Centinel running in your project in under 2 minutes.
1. Install
npm install @ejemo/centinel2. Initialize
Run the CLI to automatically set up your project. Centinel will detect your framework, language, and directory structure, then scaffold everything for you:
npx centinel initThis single command does the following:
- Detects whether you're using Next.js or Express.js from your package.json
- Detects whether you use TypeScript or JavaScript by checking for tsconfig.json
- Detects whether you use a src/ directory layout
- Creates centinel.config.json with example rules and placeholder wallets
- Generates a secure JWT_SECRET and injects it into your .env file
- For Next.js: auto-creates a proxy.ts (Next.js 16+) or middleware.ts (Next.js 13–15) file in the correct location
- For Express: prints the exact import and middleware snippet you need
3. Configure Your Wallets
Open centinel.config.json and replace the placeholder wallet addresses with your actual Solana and/or Base wallet addresses:
{
"wallets": {
"solana": "YOUR_SOLANA_WALLET_ADDRESS_HERE",
"base": "YOUR_BASE_WALLET_ADDRESS_HERE"
},
"rules": [
{
"path": "/api/scraped-data",
"price": "0.01",
"model": "per_request"
},
{
"path": "/premium-tools/*",
"price": "0.10",
"model": "per_session",
"duration": "1h"
}
],
"maxTransactionAge": 300
}4. Start Your Server
That's it. Start your development server as usual and Centinel will begin intercepting requests to the routes defined in your config.
CLI & Auto-Detection
Centinel's CLI intelligently inspects your project and scaffolds the right files in the right places.
What Gets Detected
When you run npx centinel init, the CLI reads your project directory and detects:
| Detection | How | Effect |
|---|---|---|
| Framework | Checks package.json dependencies for next or express | Determines which middleware to scaffold |
| Language | Checks for tsconfig.json | Generates .ts or .js files accordingly |
| src/ directory | Checks if a src/ folder exists | Places proxy/middleware in src/proxy.ts or root proxy.ts |
| Next.js version | Reads the next version from package.json | Generates proxy.ts (v16+) or middleware.ts (v13–15) |
What Gets Created
The CLI creates the following files automatically:
centinel.config.json
The main configuration file with placeholder wallet addresses and example paywall rules. Always created in the project root.
.env (JWT_SECRET)
A cryptographically secure 64-character hex JWT secret is generated and injected into your .env file. If the file already exists and contains a JWT_SECRET, it is left untouched.
proxy.ts / middleware.ts (Next.js only)
For Next.js projects, Centinel creates a complete Edge-compatible proxy/middleware file. The CLI auto-detects your Next.js version: for Next.js 16+ it generates proxy.ts with an exported proxy() function, and for Next.js 13–15 it generates the legacy middleware.ts. If a proxy or middleware file already exists, Centinel prints integration instructions instead of overwriting it.
Example CLI Output
��️ Initializing Centinel (x402-Connect)...
Detected framework: Next.js
Language: TypeScript
src/ directory: Yes
✅ Created centinel.config.json
✅ Created .env with JWT_SECRET and RPC config
✅ Created src/proxy.ts
ℹ️ Next.js 16+ detected — using proxy.ts convention (replaces middleware.ts)
�� Centinel setup complete! Next steps:
1. Open centinel.config.json and replace the wallet placeholders
with your Solana and/or Base wallet addresses.
2. Edit the "rules" array to define which routes are paywalled,
their prices, and payment models (per_request or per_session).Express Setup
Protect your Express.js API endpoints with the Centinel middleware.
Middleware Integration
After running npx centinel init, add the Centinel middleware to your Express app. Centinel automatically reads centinel.config.json from the project root and matches incoming requests against your rules.
import express from 'express';
import { centinelExpress } from '@ejemo/centinel';
const app = express();
// Apply Centinel middleware — reads rules from centinel.config.json
// Place this BEFORE your route handlers
app.use(centinelExpress());
// Your routes work exactly as before
app.get('/api/data', (req, res) => {
res.json({
data: "This response was paid for by the requesting agent.",
timestamp: new Date()
});
});
app.get('/premium-tools/analyze', (req, res) => {
res.json({ result: "Premium analysis complete." });
});
app.listen(3000, () => console.log('Server running on port 3000'));How It Works
- When a request arrives, Centinel checks the path against the rules array in your config.
- If the path doesn't match any rule, the request passes through untouched.
- If the path matches but no valid payment proof is found, Centinel returns a 402 Payment Required response with payment instructions.
- If the request includes valid X-Payment-Signature and X-Payment-Chain headers, Centinel verifies the transaction on-chain and grants access.
Session Token Extraction
Centinel automatically extracts session proof tokens from multiple sources in this priority order:
- x-centinel-proof cookie (works with cookie-parser or Centinel's built-in cookie reader)
- Authorization: Bearer <token> header
- X-Centinel-Proof header
Note: cookie-parser is optional. Centinel includes a built-in fallback cookie parser, so you do not need to install it as a separate dependency.
Next.js Setup
Use Centinel as Next.js Edge Middleware to enforce payment checks at the edge with zero cold starts.
Auto-Scaffolded Proxy / Middleware
When you run npx centinel init in a Next.js project, Centinel auto-detects your Next.js version and generates the correct file. Next.js 16+ uses proxy.ts (the middleware → proxy rename), while Next.js 13–15 uses the legacy middleware.ts.
Next.js 16+ (proxy.ts)
import { nextCentinel } from '@ejemo/centinel/next';
import type { NextRequest } from 'next/server';
import centinelConfig from '../centinel.config.json';
export async function proxy(request: NextRequest) {
return await nextCentinel(request, centinelConfig);
}
// Centinel runs on all routes and checks centinel.config.json
// to decide which paths require payment. No need to list paths
// here — just edit centinel.config.json to add, remove, or
// change protected routes.
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
],
};Next.js 13–15 (middleware.ts)
import { nextCentinel } from '@ejemo/centinel/next';
import type { NextRequest } from 'next/server';
import centinelConfig from '../centinel.config.json';
export async function middleware(request: NextRequest) {
return await nextCentinel(request, centinelConfig);
}
// Centinel runs on all routes and checks centinel.config.json
// to decide which paths require payment. No need to list paths
// here — just edit centinel.config.json to add, remove, or
// change protected routes.
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
],
};How the Next.js Proxy / Middleware Works
Unlike Express where Centinel reads the config file at runtime, the Next.js proxy/middleware imports centinel.config.json directly as a JSON module. This is because Next.js Edge Middleware runs in a V8 isolate that doesn't have filesystem access.
The broad matcher pattern catches all routes except static assets. Centinel then checks each request against your rules array internally — unmatched paths pass through with NextResponse.next().
Existing Proxy or Middleware File
If you already have a proxy.ts or middleware.ts file, the CLI will not overwrite it. Instead, it prints the exact code you need to integrate Centinel alongside your existing logic. If you are on Next.js 16+ but still using middleware.ts, the CLI will also warn you to migrate to proxy.ts.
import { nextCentinel } from '@ejemo/centinel/next';
import type { NextRequest } from 'next/server';
import centinelConfig from '../centinel.config.json';
// Inside your existing proxy or middleware function:
const centinelResponse = await nextCentinel(request, centinelConfig);
if (centinelResponse.status === 402) return centinelResponse;Edge Compatibility
Centinel's Next.js integration is fully Edge-compatible. It uses the native Web Crypto API for JWT signing and verification (no Node.js crypto or jsonwebtoken dependency). On-chain verification is done via raw fetch calls to JSON-RPC endpoints, with no SDK dependencies required at the Edge.
Configuration
The complete reference for centinel.config.json.
Full Config Example
{
"wallets": {
"solana": "7EcDhSwZ1mG58z9L7P9D6HtgpLhS9Kq7z4yP18WpD6aB",
"base": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
},
"rules": [
{
"path": "/api/scraped-data",
"price": "0.01",
"model": "per_request"
},
{
"path": "/premium-tools/*",
"price": "0.10",
"model": "per_session",
"duration": "1h"
}
],
"maxTransactionAge": 300
}Config Properties
| Property | Type | Required | Description |
|---|---|---|---|
| wallets.solana | string | Optional* | Solana wallet address to receive USDC/SOL payments. |
| wallets.base | string | Optional* | EVM wallet address to receive USDC/ETH payments on Base. |
| rules | array | Yes | Array of route protection rules (see below). |
| maxTransactionAge | number | No | Maximum age of a transaction in seconds before it's rejected. Prevents replay attacks. Default: 300 (5 minutes). |
* At least one wallet (Solana or Base) must be configured with a real address. If both contain placeholder values, Centinel returns a 500 configuration error instead of a 402.
Rule Properties
| Property | Type | Description |
|---|---|---|
| path | string | Glob-style route pattern. Examples: /api/data, /premium/*, /posts/*/comments |
| price | string | Cost in USDC (or equivalent in SOL/ETH) that the agent must pay. Example: "0.05" |
| model | string | Either "per_request" (pay every time) or "per_session" (pay once, access for a duration) |
| duration | string | Required for per_session. TTL for the session access token. Examples: "15m", "1h", "24h", "7d" |
Security & Verification
How Centinel protects against abuse, double-spending, and replay attacks.
On-Chain Verification
When a payment signature is submitted, Centinel performs the following checks against the actual blockchain:
- Transaction existence: Fetches the transaction from the blockchain RPC (with up to 3 retries for propagation delays).
- Transaction success: Confirms the transaction was not reverted or failed on-chain.
- Recipient match: Verifies the payment was sent to the wallet address specified in your config.
- Amount match: Verifies the transferred amount meets or exceeds the rule's price.
- Transaction age: Rejects transactions older than maxTransactionAge seconds (default: 5 minutes) to prevent replays.
- Future transaction rejection: Rejects transactions with timestamps more than 60 seconds in the future (clock manipulation protection).
Replay Protection
Centinel uses a dual-layer replay protection strategy:
- In-memory signature cache: Every verified signature is stored in a Set. Any duplicate submission is immediately rejected without an RPC call.
- Transaction age validation: Even if the cache is cleared (e.g., server restart), old transactions are rejected by the maxTransactionAge check.
Rate Limiting
To prevent bots from spamming invalid signatures and exhausting your RPC node's rate limits, Centinel includes a built-in rate limiter:
- Tracks failed verification attempts per IP address
- After 5 failed attempts within a 60-second window, the IP is temporarily blocked
- Blocked IPs receive a 429 Too Many Requests response without triggering RPC calls
- Successful verification resets the failure counter for that IP
CORS (Next.js Edge)
The Next.js middleware automatically handles CORS for cross-origin agent requests. It responds to OPTIONS preflight requests and includes Access-Control-Allow-Origin: * with all necessary payment-related headers exposed.
Environment Variables
Configure these in your .env file. The CLI auto-generates the required ones for you.
| Variable | Default | Description |
|---|---|---|
| JWT_SECRET | (auto-generated) | Required. Secret key for signing session proof JWTs. The CLI generates a secure 64-character hex string for you. |
| SOLANA_RPC_URL | https://api.mainnet-beta.solana.com | Solana RPC endpoint for verifying transactions. Replace with your own node (Helius, QuickNode, etc.) for production reliability. |
| BASE_RPC_URL | https://mainnet.base.org | Base L2 RPC endpoint for verifying EVM transactions. Replace with your own node for production reliability. |
| CENTINEL_ALLOW_MOCK | false | Set to true to explicitly allow mock signatures. Only needed if you want mocks in a production-like environment. |
Auto-Generated .env
The CLI creates or appends to your .env with the following template:
# Centinel Session JWT Key
JWT_SECRET="a1b2c3d4e5f6...your-auto-generated-64-char-hex..."
# Centinel RPC Endpoints (Optional)
# If left blank, Centinel falls back to public mainnet nodes.
# Uncomment and replace with your own reliable RPC URLs for production.
# SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
# BASE_RPC_URL="https://mainnet.base.org"Local Mocking
Test the full payment flow locally without touching the blockchain.
How Mock Signatures Work
During local development or CI testing, you can bypass real blockchain verification by sending a signature that starts with mock_. Centinel recognizes these and accepts them immediately — no RPC calls, no on-chain transactions needed.
Production Safety
Mock signatures are automatically blocked in production. They only work when:
- NODE_ENV is not set to production (the default for local dev), or
- CENTINEL_ALLOW_MOCK=true is explicitly set in the environment
If an agent sends a mock_ signature in production without the override, the request falls through to the 402 challenge (Express) or is rejected with an error message (verifier).
Testing Locally
# Test a protected endpoint with a mock payment
curl -X GET http://localhost:3000/api/scraped-data \
-H "X-Payment-Signature: mock_solana_tx_12345" \
-H "X-Payment-Chain: solana"
# You should receive the actual API response, not a 402Mock in Code
// Example: Agent-side test script
const response = await fetch('http://localhost:3000/api/data', {
headers: {
'X-Payment-Signature': 'mock_test_signature_001',
'X-Payment-Chain': 'solana',
},
});
const data = await response.json();
console.log(data); // Your actual API responseWebhooks & Callbacks
Trigger notifications, synchronize API quotas, and record transactions in your database when payments are verified.
1. Programmatic Callbacks
You can register an onPaymentVerified callback directly in the middleware options. Centinel runs this function asynchronously upon verification success, ensuring it does not block the client's API response.
Express Setup
import express from 'express';
import { centinelExpress } from '@ejemo/centinel';
const app = express();
app.use(centinelExpress({
onPaymentVerified: async (payment) => {
console.log(`Received $${payment.price} payment on ${payment.chain}!`);
console.log(`Signature: ${payment.signature} | Path: ${payment.path}`);
// TODO: Write to your database (e.g. Prisma: db.transaction.create(...))
}
}));Next.js Setup
import { nextCentinel } from '@ejemo/centinel/next';
import type { NextRequest } from 'next/server';
import centinelConfig from '../centinel.config.json';
// Next.js 16+ uses "proxy", Next.js 13–15 uses "middleware"
export async function proxy(request: NextRequest) {
return await nextCentinel(request, centinelConfig, {
onPaymentVerified: async (payment) => {
console.log(`Verified ${payment.chain} transaction for ${payment.path}`);
}
});
}2. Webhooks (HTTP POST)
Configure Centinel to automatically send a signed JSON POST request to your webhooks receiver when payment verifications succeed.
Configuration
Add a "webhookUrl" field to your centinel.config.json:
{
"wallets": {
"solana": "7EcDhSwZ1mG58z9L7P9D6HtgpLhS9Kq7z4yP18WpD6aB"
},
"rules": [
{ "path": "/api/scraped-data", "price": "0.01", "model": "per_request" }
],
"webhookUrl": "https://api.yourdomain.com/webhooks/centinel"
}Or pass it programmatically in the middleware configuration options:
app.use(centinelExpress({
webhookUrl: 'https://api.yourdomain.com/webhooks/centinel'
}));Webhook Payload format
{
"event": "payment.verified",
"timestamp": 1716388421,
"payment": {
"signature": "3u7sDf8...",
"chain": "solana",
"price": "0.01",
"path": "/api/scraped-data"
}
}Webhook Verification (Security)
To verify that a webhook POST actually originated from your Centinel server, check the signature included in the X-Centinel-Signature header.
Centinel computes a **HMAC-SHA256** hash of the raw request body using your secret key. The signing secret is read from process.env.CENTINEL_WEBHOOK_SECRET (falling back to your JWT_SECRET).
import express from 'express';
import crypto from 'crypto';
const app = express();
// Use raw body parser to get the exact unparsed JSON string
app.post('/webhooks/centinel', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-centinel-signature'];
const secret = process.env.CENTINEL_WEBHOOK_SECRET || process.env.JWT_SECRET;
const computedSignature = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (signature !== computedSignature) {
return res.status(401).send('Invalid signature');
}
// Signature matches! Safe to parse body and record payment
const payload = JSON.parse(req.body.toString());
console.log('Valid payment webhook received:', payload.payment.signature);
res.sendStatus(200);
});