npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

x402-toll

v0.1.2

Published

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

Readme

x402toll

License Node

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)