DEV Community

Cover image for How We Built x402 v2 Paid APIs on Cloudflare Workers — 3 Settled Transactions, Real USDC, Zero Auth
Revnuvo Technologies Ltd
Revnuvo Technologies Ltd

Posted on

How We Built x402 v2 Paid APIs on Cloudflare Workers — 3 Settled Transactions, Real USDC, Zero Auth

Building pay-per-request domain-intelligence APIs with no API keys, no rate limits, and no billing portal — just HTTP 402 and a signed EIP-3009 authorization on Base.

  1. The problem: API authentication is broken Every developer who has shipped a paid API knows the drill. You build the endpoint, then spend the next week bolting on the commerce layer: An API key table, with provisioning, rotation, and revocation endpoints. A rate limiter — usually a sliding window in Redis — because keys alone don't stop abuse. A metering pipeline: a Kafka topic, a counter service, a usage aggregator, a reconciliation job that runs nightly and occasionally disagree with Stripe. A billing system: monthly invoices, proration, dunning emails, tax handling for 40 jurisdictions. A customer portal where users can find their key, see their usage, and argue about overages. Each of those pieces is a separate failure surface, a separate database, and a separate pager. And the user pays for all of it: they have to sign up, wait for a key, read a rate-limit header, guess at next month's bill, and trust the metering. For an AI agent calling 10,000 endpoints a day, this is friction on top of friction. What we wanted was simpler: one HTTP call, one payment, done. No account, no key, no quota. The protocol already has a status code for that — 402 Payment Required — and it's been sitting unused in the spec since 1991. x402 finally turns it on.
  2. The x402 solution x402 is an open protocol that turns HTTP 402 into a real payment mechanism. The flow is three round-trips: Challenge. The client sends the request. The server replies with HTTP 402, a WWW-Authenticate: x402 header, and a JSON body describing what it accepts: { x402Version, scheme, network, asset, maxAmountRequired, payTo, resource, description, quote }. Sign. The client signs an EIP-3009 TransferWithAuthorization typed-data payload with their EOA private key. No on-chain transaction yet — just a signature that authorizes the payee to pull the funds. Settle. The client retries the original request with an X-Payment header carrying the signed authorization. The server forwards it to a facilitator, which verifies the signature, reserves the nonce (so it can't be replayed), broadcasts transferWithAuthorization on Base, waits for confirmation, and only then releases the response. No keys. No accounts. No metering database — the chain is the meter. A request that isn't paid simply doesn't execute.
  3. Architecture
┌──────────┐   POST (no payment)      ┌─────────────────────────────────────┐
│  Client  │ ───────────────────────▶ │  Cloudflare Worker (edge)           │
│ (curl /  │                          │  Hono router                        │
│  SDK /   │   ◀── 402 + signed quote │  + @x402/hono@2.17.0 middleware     │
│  agent)  │                          │  + ExactEvmScheme                   │
│          │   POST + X-Payment       │  + HTTPFacilitatorClient            │
│          │ ───────────────────────▶ │                                     │
│          │                          │  worker → facilitator.xpay.sh       │
│          │   ◀── 200 + body +       │    /quote  /verify  /settle         │
│          │       X-Payment-Response │                                     │
└──────────┘                          └─────────────────────────────────────┘
                                                          │
                                                          ▼
                                              ┌────────────────────────┐
                                              │ Base mainnet (8453)    │
                                              │ USDC TransferWithAuth  │
                                              │ (EIP-3009)             │
                                              └────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The worker never touches a private key, never signs a transaction, and never speaks to an RPC node directly. All of that lives in the facilitator. The worker is a thin, stateless edge proxy whose only job is to (a) emit the 402 challenge and (b) forward the client's X-Payment header to the facilitator's /verify + /settle endpoints.

  1. The implementation 4.1 Middleware setup The entire payment layer is five lines of TypeScript. The paymentMiddleware from @x402/hono@2.17.0 takes a route map (path → price + scheme + network + payTo) and a resourceServer registered against a chain-specific scheme:
const facilitator = new HTTPFacilitatorClient({ url: "https://facilitator.xpay.sh" });
const resourceServer = new x402ResourceServer(facilitator)
  .register("eip155:8453", new ExactEvmScheme());
await resourceServer.initialize();

app.use(paymentMiddleware({
  "POST /assess/domain": {
    accepts: { scheme: "exact", price: "$0.05", network: "eip155:8453", payTo },
  },
}, resourceServer));
Enter fullscreen mode Exit fullscreen mode

ExactEvmScheme is the "exact-amount EVM transfer" scheme — deterministic, replay-safe, and the right primitive for a fixed-price-per-call API. HTTPFacilitatorClient is what talks to the xpay facilitator. Everything else (challenge signing, nonce reservation, on-chain broadcast, confirmation) happens server-side at facilitator.xpay.sh.
4.2 The v2 payment payload
The 402 challenge body follows the x402 v2 shape — every field a client needs to sign without a second round-trip:

{
  "x402Version": 2,
  "scheme": "exact",
  "network": "eip155:8453",
  "payTo": "0x2aaD494F3f2f3f30E464cB84442924d764f19CE7",
  "maxAmountRequired": "50000",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "resource": "https://assess.revnuvo.site/assess/domain",
  "description": "Domain trust and risk assessment",
  "maxTimeoutSeconds": 120,
  "quote": {
    "quoteId": "q_...",
    "resource": "...",
    "scheme": "exact",
    "network": "eip155:8453",
    "asset": "0x8335...",
    "payTo": "0x2aaD...",
    "maxAmountRequired": "50000",
    "issuedAt": 1784...,
    "expiresAt": 1784...,
    "sig": "0x..."
  }
}
Enter fullscreen mode Exit fullscreen mode

The quote sub-object is HMAC-signed by the facilitator. The client never trusts the outer fields alone — it signs against quote.maxAmountRequired, quote.payTo, and quote.asset, and the facilitator rejects any payment whose signature doesn't match the quote it signed. That's what binds the payment to a specific resource + price: not a server-minted nonce, but the signed quote itself.
4.3 The CORS lesson
Our first deployment to assess.revnuvo.site worked perfectly for curl and Postman — payments settled, USDC moved — but x402scan reported "no x402 paywall detected." A false negative: the paywall demonstrably worked (Basescan showed the tx), but the crawler couldn't see it.
Root cause: our CORS config was an allowlist of three specific origins (developers.revnuvo.site, localhost:3000, 127.0.0.1:3000). The crawler's origin wasn't on the list, so the worker omitted the Access-Control-Allow-Origin header, the browser blocked the response, and the crawler saw nothing.
The fix was one line — origin: "*" — which is what dns.revnuvo.site had been using all along and had registered cleanly. The mental flip: CORS does not protect a paid endpoint. CORS is a browser-side mechanism that doesn't affect server-side callers (curl, Postman, backends). The x402 payment layer is what actually gates access. Opening CORS to all origins doesn't reduce security because the endpoint is already paywalled — it just makes the paywall legible to browser-based tooling.
4.4 The wildcard-method lesson
The second gotcha was route registration. We initially keyed our route map as "POST /assess/domain" — precise and correct for production traffic. But x402scan's crawler (and most discovery tools) probe with a GET or HEAD first to confirm a paywall exists before recording the endpoint. A POST-only route map means the probe sails past the payment middleware, hits a 404, and the endpoint looks unpaywalled.
The pattern that registers cleanly: declare routes with a wildcard method ("* /assess/domain") in the middleware route map, and let your own handler short-circuit non-POST methods with a 405 after the 402 challenge has been served. The crawler gets its 402, the discovery listing populates, and real clients still get correct method validation. Lesson: the payment middleware must see every method the route responds to, including the probe methods.
4.5 End-to-end with curl

# Step 1: the 402 challenge
curl -i -X POST https://assess.revnuvo.site/assess/domain \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}'

# HTTP/1.1 402 Payment Required
# WWW-Authenticate: x402 requirements=...
# Content-Type: application/json
# { "x402Version": 2, "scheme": "exact", "payTo": "0x2aaD...", "maxAmountRequired": "50000",
#   "asset": "0x8335...", "quote": { "quoteId": "...", "sig": "0x..." } }

# Step 2: sign the EIP-3009 authorization against the quote, then retry
curl -X POST https://assess.revnuvo.site/assess/domain \
  -H "Content-Type: application/json" \
  -H "X-Payment: $(sign-eip3009 quote.json | base64)" \
  -d '{"domain":"example.com"}'

# HTTP/1.1 200 OK
# X-Payment-Response: <base64 settlement receipt>
# { "domain":"example.com", "trust_score":92, "grade":"A", "recommendation":"TRUSTED", ... }
Enter fullscreen mode Exit fullscreen mode

4.6 Paying with the SDK
The @revnuvo/x402 SDK collapses both steps into one call — it intercepts the 402, signs against the challenge, retries with X-Payment, and surfaces the final 200:

import { X402Client } from "@revnuvo/x402";

const client = new X402Client(process.env.PRIVATE_KEY as `0x${string}`);

const result = await client.pay(
  "https://assess.revnuvo.site/assess/domain",
  { domain: "example.com" },
);

console.log(result);
// { domain: "example.com", trust_score: 92, grade: "A", recommendation: "TRUSTED",
//   signals: { domain_exists: true, has_mx: true, dnssec_enabled: true, ... } }
Enter fullscreen mode Exit fullscreen mode

The SDK uses viem's signTypedData against the standard USDC TransferWithAuthorization EIP-712 type, picks a random 32-byte nonce, sets a 120-second validBefore, and base64-encodes the { quote, payment } payload into the X-Payment header. No wallet connection, no RPC call from the client — just one signature.

  1. The proof — 3 settled transactions Every endpoint has a real, on-chain settlement on Base mainnet. These aren't test transactions; they're USDC transfers that hit 0x2aaD494F3f2f3f30E464cB84442924d764f19CE7 via transferWithAuthorization on the USDC contract at 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913: /domain/verify0x450cb5d2...0f3a87 /assess/domain0x0c263f26...2b9a3 /dns/lookup (dns.revnuvo.site) — 0x5901d53f...9add5 Open any of them on Basescan and you'll see the same shape: a transferWithAuthorization call, the payer's address as from, our payTo as to, the atomic amount (5,000–50,000 units = $0.005–$0.05 at 6-decimal USDC), and the relayer's address as msg.sender. That's the entire revenue trail. No invoice, no usage record, no reconciliation job.
  2. The distribution play Shipping the APIs was half the work. The other half was making them discoverable by the two populations that matter: developers and AI agents. x402scan — the registry of x402-powered endpoints. Once the CORS and wildcard-method lessons landed, all three endpoints registered cleanly and now show up with their price, network, and last-settled tx hash. Smithery MCP — we ship a Model Context Protocol server (mcp.revnuvo.site/mcp) that exposes assess_domain, verify_domain, and dns_lookup as tools. Agents discover them via tools/list, get a 402 on unpaid tools/call, and retry with _meta["x402/payment"]. The full handshake is in our SDK's MCP e2e test. npm SDK — @revnuvo/x402 gives TypeScript developers a one-liner payAndFetch. A Python equivalent is on PyPI. Postman collection — a public collection with the 402 + payment flow pre-wired, so teams can hit the ground running without writing signing code. developers.revnuvo.site — the canonical docs hub, with OpenAPI 3.1 schemas served live from each worker's /openapi.json.
  3. What's next The paid-API primitive is now proven. The next three things on the roadmap all build on top of it: Leadgen tool. A paid /leadgen/domain endpoint that returns verified contact data (emails, phone, LinkedIn URLs) for a domain, priced at $0.10–$0.25 per call. Same x402 flow, no new infrastructure. Smartlead integration. Pipe verified leads directly into Smartlead cold-outreach campaigns via webhook — so an agent can pay → assess → enrich → push to campaign in a single autonomous loop. Customer portal. A read-only dashboard that queries the Base ledger for transfers to payTo from a connected wallet, showing total spent, per-endpoint breakdown, and tx hashes. No backend database — the chain is the source of truth, and the portal is just a Basescan viewer with better UX. The bigger thesis: for AI-agent workloads, x402 isn't a niche payment rail — it's the default API business model. Agents can't fill out a signup form, can't wait for an API key email, and can't read a rate-limit header. They can sign an EIP-3009 authorization. Once the tool layer is paywalled this way, every agent with a funded Base wallet becomes a paying customer with zero integration friction. --- Links: x402scan · x402 spec · developers.revnuvo.site · SDK repo · assess worker · resource worker · Basescan: verify · Basescan: assess · Basescan: dns

Top comments (0)