How to Add x402 Payments to a Cloudflare Worker with Hono

x402 turns the old HTTP 402 Payment Required status into a real payment flow: a client asks for a resource, the server answers with a price, the client pays in USDC and asks again. No accounts, no API keys, no monthly plans. That is exactly what an AI agent needs when it wants to call an API it has never seen before.
This guide shows the smallest server that works, then the problems we ran into after putting eight paid endpoints on the Cloudflare Workers free plan. The example below is not pseudo-code: it lives in our repository as a test and is checked against the real facilitator.
What happens on the wire
- The client calls
POST /v1/echowith no payment. The server replies402with aPAYMENT-REQUIREDheader. It is base64-encoded JSON: schemeexact, networkeip155:8453(Base), the amount in USDC base units (1000means $0.001), the USDC contract and yourpayToaddress. - The client signs an EIP-3009 authorization ("transfer up to this amount to payTo") and repeats the request with a
PAYMENT-SIGNATUREheader. - The server asks a facilitator to verify the signature, runs your handler, and settles the payment only if the handler answered below 400. A
PAYMENT-RESPONSEheader carries the receipt with the transaction hash.
The smallest working server
Install the packages with npm i hono @x402/hono @x402/evm @x402/core. On Workers add compatibility_flags = ["nodejs_compat"] to wrangler.toml, and export the Hono app as your entry point.
import { Hono } from "hono";
import { paymentMiddleware, x402ResourceServer } from "@x402/hono";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
const PAY_TO = "0xYourWalletAddress"; // the money goes straight here
const NETWORK = "eip155:8453"; // Base mainnet (USDC)
const facilitator = new HTTPFacilitatorClient({ url: "https://facilitator.payai.network" });
const server = new x402ResourceServer(facilitator).register(NETWORK, new ExactEvmScheme());
const app = new Hono();
app.use(
paymentMiddleware(
{
"POST /v1/echo": {
accepts: [{ scheme: "exact", price: "$0.001", network: NETWORK, payTo: PAY_TO }],
description: "Echoes your JSON back (demo)",
mimeType: "application/json",
},
},
server,
),
);
// Only reached after the payment was verified.
app.post("/v1/echo", async (c) => c.json({ echo: await c.req.json() }));
export default app;
Deploy it with wrangler deploy and look at the challenge:
curl -si -X POST https://your-worker.workers.dev/v1/echo \
-H 'content-type: application/json' -d '{"hi":1}' | grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d
You should see x402Version: 2 and an accepts entry with "scheme":"exact", "network":"eip155:8453", "amount":"1000" and your address in payTo.
Gotchas from running it in production
1. The first paying customer can wait 30 seconds. On the first request of each isolate, the x402 server asks the facilitator what it supports. If the facilitator is slow or rate-limits you, the very first buyer stares at a spinner. The fix is to declare locally what you support and only call the facilitator for the two things that need it, verify and settle:
class LocalSupportFacilitator {
constructor(url, network) { this.http = new HTTPFacilitatorClient({ url, timeoutMs: 20000 }); this.network = network; }
getSupported() {
return Promise.resolve({ kinds: [{ x402Version: 2, scheme: "exact", network: this.network }], extensions: [], signers: { "eip155:*": [] } });
}
verify(payload, requirements) { return this.http.verify(payload, requirements); }
settle(payload, requirements) { return this.http.settle(payload, requirements); }
}
const server = new x402ResourceServer(new LocalSupportFacilitator(url, NETWORK)).register(NETWORK, new ExactEvmScheme());
2. Validate input before you answer 2xx. Settlement happens only for responses below 400, so a 400 for a malformed body costs the caller nothing. We test this explicitly: a paid request with an invalid language code returns 400 and no PAYMENT-RESPONSE header.
3. "Code generation from strings disallowed for this context". If you add the Bazaar discovery extension, the library validates it with Ajv, which compiles schemas with new Function. The workerd runtime blocks that and logs warnings. The 402 still works. We skip the validator and ship the extensions.bazaar object as plain static JSON with the same structure.
4. Expose the payment headers to browsers. If a web page needs to read the challenge and the receipt, allow them in CORS:
import { cors } from "hono/cors";
app.use("*", cors({ origin: "*", exposeHeaders: ["PAYMENT-REQUIRED", "PAYMENT-RESPONSE"] }));
5. Keep prices in cents. Automatic payers cap how much they will spend without asking a human. @x402/fetch has a spendControls.maxAmountPerPayment that defaults to $1: we tested it against a fake 402 server, and a $1.00 challenge was paid while $5.00 was rejected with "All payment requirements were rejected by spendControls.maxAmountPerPayment". Price for agents in cents (our most expensive item is $0.99) and raise the cap on your own client if you need to.
Test it without spending a cent
Two options. First, run a tiny mock facilitator locally (ours is about 60 lines of Python that answers /verify and /settle) and point FACILITATOR_URL at it. Second, call the real facilitator with a brand-new wallet that holds no USDC: you get invalid_exact_evm_insufficient_balance, which proves your route configuration and the client payload are accepted and only the money is missing. The client side is covered in How an AI Agent Pays an x402 API.
Make people find it
A paid API nobody can find earns nothing. Publish /openapi.json with x-payment-info, add the discovery extension to your 402 and register the endpoints in the public directories. We wrote down exactly how in How to List Your x402 API on 402 Index, x402scan and Bazaar.
Everything in this series runs against a live service you can call: see the API page for the eight endpoints and their prices, from $0.001 for embeddings to $0.02 for an image.
FAQ
Do I need an API key or an account to accept x402 payments?
No. You need a receiving wallet address and a facilitator URL. The payer signs with their own wallet, so there are no accounts, API keys or sign-up forms.
Does the payer need ETH for gas?
No. x402 on EVM chains uses EIP-3009 transfer authorizations: the payer signs a message and the facilitator submits the transfer and pays the gas.
What happens if my handler fails after the payment was verified?
Return a status of 400 or higher and the payment is not settled, so the caller is not charged. Settlement only happens after a successful (below 400) response.
Which facilitator can I use without signing up?
PayAI's public facilitator at facilitator.payai.network works without keys for USDC on Base mainnet, and it is what we use. Coinbase's hosted facilitator uses CDP API keys.
Found this useful? Tip the studio in crypto
Every EVM chain works. USDC on Base is recommended: fees are a fraction of a cent. No account needed — it goes straight to the creator's wallet.
0x13dd72Fa0E7504790585D92bD98c720f6fD2aBa6More from DevNotes

How an AI Agent Pays an x402 API: a 20-Line Client in JS
A working x402 client in JavaScript: sign the USDC payment, read the receipt, cap what your agent can spend…

Cloudflare Cron Triggers Not Firing? Use a Durable Object Alarm
A reliable clock for Cloudflare Workers: a Durable Object alarm that re-arms itself, with a minimum gap and a…

How to List Your x402 API on 402 Index, x402scan and Bazaar
The exact steps to get a pay-per-call x402 API discovered by AI agents: OpenAPI metadata, 402 Index, x402scan…

Workers AI Free Tier: What Each API Call Really Costs in Neurons
Plan a pay-per-call AI API on Cloudflare's free 10,000 neurons a day: the cost of images, speech, embeddings…