npm packages are not installable yet: [email protected] can be published after 2026-09-16 10:30 UTC. Details
Tollstile

x402

The x402 V2 rail. Agents pay per request in USDC or another EVM token; Tollstile verifies through a facilitator before the handler and settles after it succeeded.

Public Beta · early access
npm install tollstile @tollstile/x402
import { createTollstile, upTo } from "tollstile";
import { tollstile } from "@tollstile/hono";
import { x402 } from "@tollstile/x402";

const toll = createTollstile({
  rails: [
    x402({
      network: "eip155:84532", // Base Sepolia
      payTo: "0xYourAddress",
      denomination: "USD", // 1 USDC = 1 USD, stated explicitly
      rpcUrl: "https://sepolia.base.org",
      upto: { facilitatorAddress: "0xd407e409E34E0b9afb99EcCeb609bDbcD5e7f1bf" }, // from GET /supported
    }),
  ],
  ledger,
  secret: process.env.TOLLSTILE_SECRET, // 32+ random characters
});

app.get("/weather", tollstile(toll.price("$0.01")), (c) => c.json({ sunny: true }));
app.post("/generate", tollstile(toll.price(upTo("$0.10"))), async (c) => {
  await c.get("payment").fulfill({ amount: "$0.03" }); // settles 0.03 USDC of the 0.10 authorized
  return c.json({ text: "…" });
});

setInterval(() => void toll.reconcile(), 60_000);

Tutorials: Monetize an API with x402 · Express · MCP tools.

Rail namex402
Schemesexact (EIP-3009 transferWithAuthorization) for fixed prices; upto (Permit2) for upTo() prices
HTTPPAYMENT-REQUIRED / PAYMENT-SIGNATURE / PAYMENT-RESPONSE, base64 JSON. Only x402Version: 2
MCPProof in _meta["x402/payment"], receipt in _meta["x402/payment-response"], payment required as an isError tool result with structuredContent
ReconciliationOn-chain, through your JSON-RPC endpoint

Options

OptionDefaultDescription
networkrequiredCAIP-2 EVM network. Built-in assets: eip155:8453 (Base, USDC) and eip155:84532 (Base Sepolia, USDC).
payTorequiredYour receiving address. Every payment is checked against it.
denominationConversion at par, e.g. "USD" for USDC. Set exactly one of denomination and rate; the built-in USDC only accepts "USD".
rate(price: Money) => Promise<bigint>: atomic asset units for a price, for assets not at par. The quote fixes the result for the payer.
assetbuilt-in USDC{ code, address, decimals, name, version } with the token's EIP-712 domain. Required on other networks; decimals 6–18.
facilitatorx402.org on Base Sepolia only{ url, headers? }. headers: () => Promise<Record<string, string>> runs per request, e.g. for a CDP JWT. Required on mainnet and every other network; the testnet facilitator is never used silently.
rpcUrlrequiredJSON-RPC endpoint for network, used only by reconciliation. Must support the finalized block tag and eth_getLogs.
uptodisabled{ facilitatorAddress } enables upTo() prices. Use the address your facilitator lists for upto in GET /supported.
maxTimeoutSeconds60How long the payer's signature is valid. The handler and settlement must both finish inside it, or settlement is rejected after the service was delivered.
fetchglobal fetchFor tests and custom transports.

Capabilities

CapabilityValueWhy
flowsauthorizationVerify, run the handler, then settle. exact cannot be refunded or voided, so settling first would charge for work that failed.
authorizationsingleOne signed authorization pays for one request. After a released charge the same payment can be retried.
variableAmounttrue with uptoPermit2 upto authorizes a maximum and settles the fulfilled amount. Without upto, x402 is excluded from upTo() routes; the route fails to compile only if no configured rail remains.
quotestrueThe quote token travels in accepts[].extra.tollstileQuote, which V2 clients echo in accepted.
refund · partialRefundfalseNeither scheme has a refund.
lookuptrueOn-chain, below.

livemode is true, including on testnets, so the rail cannot run next to the test rail.

Flow

  1. Challenge. The 402 carries a PAYMENT-REQUIRED header with the requirements for this price, including extra.tollstileQuote.
  2. Verify. The proof must be x402Version: 2. If it carries a quote, requirements are derived from the quote's offer; otherwise from the route's fixed price. accepted must equal those requirements, and the signed authorization must name payTo, the exact amount (or the upto maximum), the asset, the upto proxy as spender, and the configured facilitator. The facilitator's /verify is called with this server's requirements, never the client's.
  3. Run. The handler runs on a reservation.
  4. Settle. /settle is called with the stored payload. For upto, the amount is the fulfilled amount at the quoted ratio, rounded down.

The proof id is network:asset:payer:nonce, so a replayed payment maps to the same authorization.

Facilitator answerResult
/verify says isValid: false (as HTTP 200 or a non-2xx JSON body)402 with error code proof_invalid and the facilitator's reason, sanitized to [a-z0-9_], in error.detail
/verify unreachable, timed out, non-JSON, or unexpected_verify_error503; the handler does not run
/settle says success: falseCharge failed; the output is withheld and the client gets a fresh 402 with error code settlement_rejected
/settle answers settlement_pending or unexpected_settle_error, times out, or is unreachableCharge unknown; the output is served without a receipt and reconciliation asks the chain

Tollstile never calls /settle twice on a hunch: facilitators have no status endpoint, and /settle is not idempotent.

Lookup and reconciliation

All reads happen at the finalized block.

SchemeSettled whenNot settled when
exactauthorizationState(payer, nonce) is used, and the token's AuthorizationUsed(payer, nonce) log sits in a successful transaction with Transfer(payer, payTo, value)An AuthorizationCanceled log
uptoThe Permit2 nonce bit is set, and a Transfer(payer, payTo) log's transaction called the upto proxy with this nonce, owner, and token; the amount comes from the logAn UnorderedNonceInvalidation covering the nonce
  • Unused nonce: none only once the finalized block is past the signature's deadline, when no later block can include it. Before that the charge stays unknown until the next run.
  • Used nonce without recognizable evidence (for example, a facilitator settling through a batching contract): the charge stays unknown, with an error to investigate. Tollstile does not guess.
  • Logs are searched from the charge's creation time (minus 10 minutes of clock-skew margin) to the signature's deadline.

Stored data and redaction

The authorization's data holds the payer's signed payload, because settlement may run in another process after a crash. It never appears in errors, events, or receipts.

The rail implements redact. Once a charge is final, core replaces paymentPayload and paymentRequirements with null, keeping the scheme, network, asset, payTo, payer, nonce, deadline, and amounts that lookup needs.

  • While a charge is unknown, the payload stays, so reconciliation can still settle it.
  • A released charge is not redacted, so the same payment can be retried.

Verification status

Live verification status. The exact flow has been verified on Base Sepolia with x402.org, including a successful USDC transfer, replay rejection, handler failure followed by retry, and persistence across a process restart with the SQLite ledger. upto, reconciliation after an ambiguous settlement, and production providers still require separate verification.

  • Full flows through createTollstile with a fake facilitator and a fake JSON-RPC node sharing one simulated chain: exact and upto, dynamic prices, tampered requirements and signatures, expired quotes, facilitator outages (503), replay and concurrent replay, retry after release, rejected settlement, settlement_pending reconciled from chain evidence without a second /settle, payer cancellation, RPC outages, crash recovery, MCP challenge and receipt, and redaction.
  • Headers round-trip through @x402/core 2.25.0, and the reference x402ResourceServer.findMatchingRequirements accepts what the rail advertises.

Not yet checked live: signature acceptance by x402.org, real facilitator error bodies, your RPC provider's finalized behavior and log range limits, and upto settlement through settleWithPermit. The tutorial walks through a Base Sepolia run; the package README lists replay, failure, and reconciliation checks.

Retries and payer identity

The payer is the lowercase EVM address. Send Idempotency-Key from the first paid request. If absent, the rail can use extensions["payment-identifier"].info.id from the payment payload. The HTTP/MCP client key takes precedence.

A successfully settled and fulfilled retry with a key returns 409 already_paid; without either key, a non-released single-use proof returns 409 proof_already_used. A genuine used proof rejected by the provider can still identify its authorization so core answers from the ledger instead of issuing a new payment challenge. Invalid or unidentifiable proofs still return 402.

In-flight keyed charges return 409 request_in_progress; unknown outcomes return 503 payment_outcome_unknown. See Idempotency.

On this page