AI-Ready Docs: Feed these docs directly into your AI assistant (Claude, GPT, Cursor, Copilot). Get the complete reference as a single markdown file.
llms.txt

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

  1. Request: An agent or bot sends a request to a protected route.
  2. Challenge: Centinel intercepts the request and returns HTTP 402 with payment instructions in WWW-Authenticate headers and a JSON body.
  3. 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.
  4. 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.
  5. 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

bash
npm install @ejemo/centinel
1

2. Initialize

Run the CLI to automatically set up your project. Centinel will detect your framework, language, and directory structure, then scaffold everything for you:

bash
npx centinel init
1

This 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:

centinel.config.json
{
  "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
}
1234567891011121314151617181920

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:

DetectionHowEffect
FrameworkChecks package.json dependencies for next or expressDetermines which middleware to scaffold
LanguageChecks for tsconfig.jsonGenerates .ts or .js files accordingly
src/ directoryChecks if a src/ folder existsPlaces proxy/middleware in src/proxy.ts or root proxy.ts
Next.js versionReads the next version from package.jsonGenerates 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

Terminal
  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).
1234567891011121314151617

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.

server.ts
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'));
12345678910111213141516171819202122

How It Works

  1. When a request arrives, Centinel checks the path against the rules array in your config.
  2. If the path doesn't match any rule, the request passes through untouched.
  3. If the path matches but no valid payment proof is found, Centinel returns a 402 Payment Required response with payment instructions.
  4. 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)

src/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)$).*)',
  ],
};
1234567891011121314151617

Next.js 13–15 (middleware.ts)

src/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)$).*)',
  ],
};
1234567891011121314151617

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.

Integration snippet
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;
1234567

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

centinel.config.json
{
  "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
}
1234567891011121314151617181920

Config Properties

PropertyTypeRequiredDescription
wallets.solanastringOptional*Solana wallet address to receive USDC/SOL payments.
wallets.basestringOptional*EVM wallet address to receive USDC/ETH payments on Base.
rulesarrayYesArray of route protection rules (see below).
maxTransactionAgenumberNoMaximum 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

PropertyTypeDescription
pathstringGlob-style route pattern. Examples: /api/data, /premium/*, /posts/*/comments
pricestringCost in USDC (or equivalent in SOL/ETH) that the agent must pay. Example: "0.05"
modelstringEither "per_request" (pay every time) or "per_session" (pay once, access for a duration)
durationstringRequired 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:

  1. Transaction existence: Fetches the transaction from the blockchain RPC (with up to 3 retries for propagation delays).
  2. Transaction success: Confirms the transaction was not reverted or failed on-chain.
  3. Recipient match: Verifies the payment was sent to the wallet address specified in your config.
  4. Amount match: Verifies the transferred amount meets or exceeds the rule's price.
  5. Transaction age: Rejects transactions older than maxTransactionAge seconds (default: 5 minutes) to prevent replays.
  6. 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.

VariableDefaultDescription
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_URLhttps://api.mainnet-beta.solana.comSolana RPC endpoint for verifying transactions. Replace with your own node (Helius, QuickNode, etc.) for production reliability.
BASE_RPC_URLhttps://mainnet.base.orgBase L2 RPC endpoint for verifying EVM transactions. Replace with your own node for production reliability.
CENTINEL_ALLOW_MOCKfalseSet 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:

.env
# 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"
12345678

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

Terminal
# 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 402
123456

Mock in Code

test-agent.ts
// 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 response
12345678910

Webhooks & 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

server.ts
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(...))
  }
}));
123456789101112

Next.js Setup

src/proxy.ts
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}`);
    }
  });
}
123456789101112

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:

centinel.config.json
{
  "wallets": {
    "solana": "7EcDhSwZ1mG58z9L7P9D6HtgpLhS9Kq7z4yP18WpD6aB"
  },
  "rules": [
    { "path": "/api/scraped-data", "price": "0.01", "model": "per_request" }
  ],
  "webhookUrl": "https://api.yourdomain.com/webhooks/centinel"
}
123456789

Or pass it programmatically in the middleware configuration options:

server.ts
app.use(centinelExpress({
  webhookUrl: 'https://api.yourdomain.com/webhooks/centinel'
}));
123

Webhook Payload format

Webhook Payload
{
  "event": "payment.verified",
  "timestamp": 1716388421,
  "payment": {
    "signature": "3u7sDf8...",
    "chain": "solana",
    "price": "0.01",
    "path": "/api/scraped-data"
  }
}
12345678910

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).

webhook-receiver.ts
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);
});
12345678910111213141516171819202122232425