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.
npm install tollstile @tollstile/x402import { 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 name | x402 |
| Schemes | exact (EIP-3009 transferWithAuthorization) for fixed prices; upto (Permit2) for upTo() prices |
| HTTP | PAYMENT-REQUIRED / PAYMENT-SIGNATURE / PAYMENT-RESPONSE, base64 JSON. Only x402Version: 2 |
| MCP | Proof in _meta["x402/payment"], receipt in _meta["x402/payment-response"], payment required as an isError tool result with structuredContent |
| Reconciliation | On-chain, through your JSON-RPC endpoint |
Options
| Option | Default | Description |
|---|---|---|
network | required | CAIP-2 EVM network. Built-in assets: eip155:8453 (Base, USDC) and eip155:84532 (Base Sepolia, USDC). |
payTo | required | Your receiving address. Every payment is checked against it. |
denomination | — | Conversion 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. |
asset | built-in USDC | { code, address, decimals, name, version } with the token's EIP-712 domain. Required on other networks; decimals 6–18. |
facilitator | x402.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. |
rpcUrl | required | JSON-RPC endpoint for network, used only by reconciliation. Must support the finalized block tag and eth_getLogs. |
upto | disabled | { facilitatorAddress } enables upTo() prices. Use the address your facilitator lists for upto in GET /supported. |
maxTimeoutSeconds | 60 | How 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. |
fetch | global fetch | For tests and custom transports. |
Capabilities
| Capability | Value | Why |
|---|---|---|
flows | authorization | Verify, run the handler, then settle. exact cannot be refunded or voided, so settling first would charge for work that failed. |
authorization | single | One signed authorization pays for one request. After a released charge the same payment can be retried. |
variableAmount | true with upto | Permit2 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. |
quotes | true | The quote token travels in accepts[].extra.tollstileQuote, which V2 clients echo in accepted. |
refund · partialRefund | false | Neither scheme has a refund. |
lookup | true | On-chain, below. |
livemode is true, including on testnets, so the rail cannot run next to the test rail.
Flow
- Challenge. The
402carries aPAYMENT-REQUIREDheader with the requirements for this price, includingextra.tollstileQuote. - 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.acceptedmust equal those requirements, and the signed authorization must namepayTo, the exact amount (or theuptomaximum), the asset, the upto proxy as spender, and the configured facilitator. The facilitator's/verifyis called with this server's requirements, never the client's. - Run. The handler runs on a reservation.
- Settle.
/settleis called with the stored payload. Forupto, 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 answer | Result |
|---|---|
/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_error | 503; the handler does not run |
/settle says success: false | Charge 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 unreachable | Charge 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.
| Scheme | Settled when | Not settled when |
|---|---|---|
exact | authorizationState(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 |
upto | The 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 log | An UnorderedNonceInvalidation covering the nonce |
- Unused nonce:
noneonly once the finalized block is past the signature's deadline, when no later block can include it. Before that the charge staysunknownuntil 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
releasedcharge 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
createTollstilewith a fake facilitator and a fake JSON-RPC node sharing one simulated chain:exactandupto, dynamic prices, tampered requirements and signatures, expired quotes, facilitator outages (503), replay and concurrent replay, retry after release, rejected settlement,settlement_pendingreconciled from chain evidence without a second/settle, payer cancellation, RPC outages, crash recovery, MCP challenge and receipt, and redaction. - Headers round-trip through
@x402/core2.25.0, and the referencex402ResourceServer.findMatchingRequirementsaccepts 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.