Skip to content

Developerayo/x402toll

Repository files navigation

x402toll

npm version CI License

First classify the caller, then allow, block, or charge all via x402

x402toll is a middleware library that decides whether each request is either a human, a verified bot, verified AI agent, claimed-but-unverified, or an unknown automation, and routes accordingly.

x402toll is not a bot-detector. It is not an x402 client. x402toll sits in between both.

Install

npm install x402-toll @x402/core

Optional:

npm install web-bot-auth
npm install ioredis

Needed: Node 20+. Next.js 14, 15, or 16. The Edge adapter we have has the same API as the Node adapter but skips the reverse DNS verification. node:dns/promises as you know is not available on edge.

Getting Started

Express

import express from "express";
import { x402toll } from "x402-toll/express";
import { HTTPFacilitatorClient } from "@x402/core/http";

const app = express();

app.use(
  x402toll({
    payTo: "0xYourWalletAddress",
    network: "eip155:8453",
    facilitator: new HTTPFacilitatorClient({
      url: "https://api.cdp.coinbase.com/platform/v2/x402",
    }),
    policy: [
      { match: { class: "human" }, action: { kind: "allow" } },
      {
        match: { class: "verified-bot", vendor: "google" },
        action: { kind: "allow" },
      },
      {
        match: { class: "verified-bot", vendor: "bing" },
        action: { kind: "allow" },
      },
      {
        match: { class: "verified-agent", vendor: "openai" },
        action: { kind: "charge", price: "$0.01" },
      },
      {
        match: { class: "verified-agent", vendor: "anthropic" },
        action: { kind: "charge", price: "$0.01" },
      },
      {
        match: { class: "claimed-but-unverified" },
        action: { kind: "charge", price: "$0.05" },
      },
      {
        match: { class: "unknown-automation" },
        action: { kind: "charge", price: "$0.10" },
      },
    ],
  }),
);

Next.js

import { x402toll } from "x402-toll/next";
import { HTTPFacilitatorClient } from "@x402/core/http";

const toll = x402toll({
  payTo: "0xYourWalletAddress",
  network: "eip155:8453",
  facilitator: new HTTPFacilitatorClient(),
  policy: [
    { match: { class: "human" }, action: { kind: "allow" } },
    {
      match: { class: "verified-bot", vendor: "google" },
      action: { kind: "allow" },
    },
    {
      match: { class: "verified-bot", vendor: "bing" },
      action: { kind: "allow" },
    },
    {
      match: { class: "verified-agent", vendor: "openai" },
      action: { kind: "charge", price: "$0.01" },
    },
    {
      match: { class: "verified-agent", vendor: "anthropic" },
      action: { kind: "charge", price: "$0.01" },
    },
    {
      match: { class: "claimed-but-unverified" },
      action: { kind: "charge", price: "$0.05" },
    },
    {
      match: { class: "unknown-automation" },
      action: { kind: "charge", price: "$0.10" },
    },
  ],
});

export default async function middleware(request) {
  return toll(request);
}

For the Edge runtime, you'll swap the import: import { x402toll } from 'x402-toll/next/edge'. The edge adapter drops the reverse dns check

How it works

For every request, x402toll runs 3 short steps:

  1. Classify — We evaluate the req against up to four signals: The User Agent pattern, IP CIDR matching, forward confirmed rDNS, and Web Bot Auth signature. The strongest one wins, signals don't combine
  2. Decide — We then match the verdict against your set policy rules in order. First match wins. No match defaults to allowing the traffic normally.
  3. Settle — On charge, return 402 with the payment requirements. The client then retries with payment attached, your facilitator verifies, then settles on-chain

The whole thing is a single func: (input) → Promise<TollOutcome>.

Status codes

Code When
200 Allow path, or successful settle
402 Charge, payment required with PAYMENT-REQUIRED
403 Policy block action, caller refused bythe rules you set
502 Facilitator settle failed (settle_failed)
503 Facilitator verify unavailable (verify_failed), with Retry-After: 5

How classification works

Signal Class Confidence
Request is signed, but the signature doesn't check out unknown-automation 0.99 (veto)
Request is signed and the signature is valid verified-agent 0.99
Reverse DNS points back to the vendor and forward resolves verified-bot 0.95
IP CIDR falls inside the vendor's published ranges verified-bot / agent 0.90
User Agent matches a known vendor pattern but no IP/DNS claimed-but-unverified 0.70
isbot flags it as a generic bot unknown-automation 0.60
Nothing matched human 0.50

Web Bot Auth (WBA) runs first whenever its headers are there. A forged signature vetoes any later verdict the weaker confidence would have produced

No default wba adapter ships yet, you pass your own verifier that implements WbaVerifier. Leave it off and WBA sits out, the other three signals run as normal

Policy

Policy is plain TypeScript object. A rule is { match, action }. Every field in match must match logical AND, action are allow, block, or charge

type PolicyRule = {
  match: {
    class?: 'human' | 'verified-bot' | 'verified-agent' | 'claimed-but-unverified' | 'unknown-automation';
    vendor?: 'google' | 'bing' | 'openai' | 'anthropic' | /* others ... */;
    confidence?: { gte: number };
  };
  action:
    | { kind: 'allow' }
    | { kind: 'block' }
    | { kind: 'charge'; price: `$${number}` };
};

The price on a charge action is a $-prefixed USD string like $0.01. It resolves to atomic units (6 decimals) per the chosen asset.

Config

Field Default What
payTo required The recipient 0x address
network required CAIP 2 id (e.g. eip155:8453)
facilitator required An @x402/core FacilitatorClient (e.g. HTTPFacilitatorClient)
policy required Ordered list of { match, action } rules
asset USDC for the network ERC20 token 0x address
maxTimeoutSeconds 60 Payment timeout window
wire default v2 codec Custom WireCodec for PAYMENT-REQUIRED / SIGNATURE / RESPONSE
session off JWT receipts, read more down
signals UA + IP + rDNS (+ WBA if wired) Per signal toggles / timeouts
registry built in vendor registry Custom CIDR registry
receipts.store in-memory LRU Redis or any ReceiptStore
logger JSON to stderr Custom Logger
wbaVerifier off Your Web Bot Auth verifier

The library ships a USDC asset for Base, Base Sepolia, Arbitrum (One + Sepolia), Polygon (+ Amoy), World Chain (+ Sepolia), and Ethereum. Pass asset to override or to target any other ERC20

Sessions

Without sessions every request runs the full x402 verify and settle flow everytime, even when the same caller hits the same paid route over and over. in our case, a paid caller gets a JWT so they don't re pay for every request they make. The default ttl is 1 hour, scoped to the same route group "network, scheme, payTo, price"

Session tokens are bearer credentials. A leaked x402toll-session header or cookie for example spends the budget for whoever holds it until the expiry

x402toll({
  ...
  session: {
    enabled: true,
    secret: [process.env.X402_SECRET, process.env.X402_SECRET_PREV],
    maxAge: 3600,
    maxRequests: 100,
  },
});

For multi replica deployments for e.g, you'll swap the in memory store for Redis

import Redis from "ioredis";
import { createRedisStore } from "x402-toll/store-redis";

x402toll({
  ...
  receipts: {
    store: createRedisStore({ client: new Redis(process.env.REDIS_URL) }),
  },
});

Side Note: The in-memory store uses LRU cleanup at 10,000 entries. Refusing on overflow would let a person for example pay once and lock a slot, LRU evicts them first, paid once, never came back is exactly least recently used.

Current coverage

From verfied sources

Vendor Source Class
OpenAI openai.com/{gptbot,searchbot,chatgpt-user}.json verified-agent
Anthropic claude.com/crawling/bots.json verified-agent
Google developers.google.com/search/apis/ipranges/{googlebot,special-crawlers,user-triggered-fetchers}.json verified-bot
Perplexity published ranges verified-agent
Meta no published CIDR source claimed-but-unverified
Bing bing.com/toolbox/bingbot.json verified-bot
Apple search.developer.apple.com/applebot.json verified-bot
Common Crawl published ranges verified-bot
DuckDuckGo duckduckgo.com/duckduckbot.json verified-bot
Yandex rDNS only (.yandex.com / .net / .ru) verified-bot
LinkedIn rDNS only (.fwd.linkedin.com) verified-bot
Baidu rDNS only (.baidu.com / .baidu.jp) verified-bot
Sogou rDNS only (.sogou.com) verified-bot
Seznam rDNS only (.seznam.cz) verified-bot
Mistral mistral.ai/mistralai-{user,index}-ips.json verified-agent

Meta has no public CIDR source, so it maxes out at claimed-but-unverified via UA. Wire a WbaVerifier and it hits verified-agent at 0.99

User Agent patterns: GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-User, anthropic-ai, PerplexityBot, Meta-ExternalAgent, Meta-ExternalFetcher, Googlebot, bingbot, Applebot-Extended, Applebot, CCBot, DuckDuckBot, YandexBot, LinkedInBot, Baiduspider, Sogou web/inst/news spider, SeznamBot, MistralAI-User, MistralAI-Index. Generic bots are caught by [isbot](https://www.npmjs.com/package/isbot).

Not supported yet: xAI / Grok, Brave Search, ByteDance / Bytespider — none publish verifiable IPs, rDNS suffixes, or signed identities. Amazon's three crawlers (Amazonbot, Amzn-SearchBot, Amzn-User) publish IPs in HTML, an adapter is planned

Public

import {
  createToll,
  type X402TollConfig,
  type TollOutcome,
  type TollSignals,
  type FacilitatorClient,
  type FacilitatorConfig,
  type PaymentRequirements,
  type PaymentRequired,
  type PaymentPayload,
  type VerifyResponse,
  type SettleResponse,
  type WireCodec,
  HTTPFacilitatorClient,
  defaultWireCodec,
  usdcAssetFor,
} from "x402-toll";

// adapters
import { x402toll } from "x402-toll/express";
import { x402toll } from "x402-toll/next";
import { x402toll } from "x402-toll/next/edge";

import {
  uaSignal,
  ipSignal,
  rdnsSignal,
  webBotAuthSignal,
  type WbaVerifier,
} from "x402-toll/signals";

import {
  createRegistry,
  createDefaultRegistry,
  type Registry,
} from "x402-toll/registry";

import { createRedisStore } from "x402-toll/store-redis";

Logging

{
  "ts": "2026-05-10T12:00:00.000Z",
  "rid": "...",
  "route": "/api/data",
  "ip": "1.2.3.4",
  "ua": "GPTBot/1.1",
  "class": "verified-agent",
  "vendor": "openai",
  "conf": 0.9,
  "decision": "charge",
  "price": "$0.01",
  "network": "eip155:8453",
  "chain": "Base",
  "tx": "0x...",
  "evidence": [
    { "type": "ip_match", "vendor": "openai", "cidr": "40.84.180.224/28" }
  ],
  "ms": 3
}

Networks

Any EVM chain that your facilitator supports. Common ones:

Network CAIP-2 USDC
Arbitrum One eip155:42161 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
Arbitrum Sepolia eip155:421614 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
Base mainnet eip155:8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Base Sepolia eip155:84532 0x036CbD53842c5426634e7929541eC2318f3dCF7e
Polygon eip155:137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Polygon Amoy eip155:80002 0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582
World Chain eip155:480 0x79A02482A880bCe3F13E09da970dC34dB4cD24D1
World Chain Sepolia eip155:4801 0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88

Simulate a request against a running server:

curl -H "User-Agent: GPTBot/1.1" http://localhost:3000/api/v1 -i

You'll see one JSON log line on stderr and a status code per the Status codes table above

License

MIT. See LICENSE

SPDX-License-Identifier: (MIT OR CC0-1.0)

About

Classify the caller, then allow, block, or charge via x402

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors