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.
- 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. - 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, aWWW-Authenticate: x402header, and a JSON body describing what it accepts:{ x402Version, scheme, network, asset, maxAmountRequired, payTo, resource, description, quote }. Sign. The client signs an EIP-3009TransferWithAuthorizationtyped-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 anX-Paymentheader carrying the signed authorization. The server forwards it to a facilitator, which verifies the signature, reserves the nonce (so it can't be replayed), broadcaststransferWithAuthorizationon 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. - 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) │
└────────────────────────┘
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.
- The implementation
4.1 Middleware setup
The entire payment layer is five lines of TypeScript. The
paymentMiddlewarefrom@x402/hono@2.17.0takes a route map (path → price + scheme + network + payTo) and aresourceServerregistered 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));
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..."
}
}
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", ... }
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, ... } }
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.
- 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
0x2aaD494F3f2f3f30E464cB84442924d764f19CE7viatransferWithAuthorizationon the USDC contract at0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913:/domain/verify—0x450cb5d2...0f3a87/assess/domain—0x0c263f26...2b9a3/dns/lookup(dns.revnuvo.site) —0x5901d53f...9add5Open any of them on Basescan and you'll see the same shape: atransferWithAuthorizationcall, the payer's address asfrom, ourpayToasto, the atomic amount (5,000–50,000 units = $0.005–$0.05 at 6-decimal USDC), and the relayer's address asmsg.sender. That's the entire revenue trail. No invoice, no usage record, no reconciliation job. - 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 exposesassess_domain,verify_domain, anddns_lookupas tools. Agents discover them viatools/list, get a 402 on unpaidtools/call, and retry with_meta["x402/payment"]. The full handshake is in our SDK's MCP e2e test. npm SDK —@revnuvo/x402gives TypeScript developers a one-linerpayAndFetch. 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. - 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/domainendpoint 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 canpay → assess → enrich → push to campaignin a single autonomous loop. Customer portal. A read-only dashboard that queries the Base ledger for transfers topayTofrom 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)