X402 Battle Scars
Shipping x402 on Solana: three failures the docs don't mention I put a paper-trading lab's telemetry up for sale over x402 - 8 endpoints on a Cloudflare Worker, $0.01-$0.02 per request, USDC on Solana mainnet, listed in the CDP Bazaar. The happy path took an afternoon. Getting real settlements to work took three genuinely undocumented failures. This is the writeup I wish had existed. Quick primer if you haven't touched x402 v2: a paid endpoint answers 402 with a PAYMENT-REQUIRED header (base64 JSON payment requirements). The client signs a payment against those requirements and retries with a PAYMENT-SIGNATURE header. A facilitator (I use Coinbase's CDP facilitator; PayAI also settles Solana) verifies and settles on-chain, and the 200 comes back with a settlement receipt in PAYMENT-RESPONSE . On EVM chains this flow is forgiving. On Solana, three of its assumptions break. 1. Your payout address needs a USDC token account before the first sale Symptom: every buy attempt returns 402 again with transaction_simulation_failed . No other detail. The buyer's wallet is funded, the requirements decode cleanly, the facilitator is reachable - it just refuses, identically, every time. I burned three attempts theorizing about wallet bugs and payload corruption before checking the obvious-in-hindsight thing: the payout address had never held USDC, so it had no associated token account (ATA) for the USDC mint (EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v ). The facilitator simulates the transfer during verification; a transfer into a nonexistent ATA fails simulation; the facilitator rejects before anything hits the chain. Fix: send your own payout address a few cents of USDC once. The transfer creates the ATA and every subsequent sale settles. (Nothing is lost during the failures - verification rejects before any money moves.) Lesson: on EVM, any address can receive tokens. On Solana, receiving is a stateful capability you set up. If you're building an x402 seller with a fresh payout wallet, fund its ATA as part of deployment, not as a debugging epiphany. 2. CDP rotates the Solana feePayer - and your serverless isolates each cache their own This one is the reason this post exists. On Solana, the facilitator sponsors transaction fees, and its fee-payer pubkey is embedded in the payment requirements your server hands out (extra.feePayer ). Your resource server learns that value from the facilitator's /supported endpoint. Symptom: every purchase through the CDP facilitator bounced with verification failures, while the exact same code settled fine through PayAI. Six consecutive curl s against my Worker's 402 endpoint showed three different feePayers. What's happening: CDP rotates the feePayer across /supported calls, PayAI's is static. And on Cloudflare Workers (or Lambda, or any serverless runtime), each isolate fetches and caches /supported independently. So isolate A builds your 402 with feePayer X, the buyer signs a transaction naming X, the retry lands on isolate B which knows feePayer Y - and verification fails. It's a distributed-cache coherence bug wearing a payments costume, and it's invisible until you have both a rotating facilitator and more than one isolate. Fix: pin one /supported snapshot in shared storage (KV) so every isolate serves the same feePayer. TTL comfortably above the payment-proof validity window (mine: 20 min pin vs 300 s proof window): class CachedFacilitatorClient extends HTTPFacilitatorClient { constructor(config: FacilitatorConfig, private kv: KVNamespace, private cacheKey: string) { super(config); } override async getSupported(): Promise { const cached = await this.kv.get (this.cacheKey, "json").catch(() => null); if (cached) return cached; const fresh = await super.getSupported(); await this.kv.put(this.cacheKey, JSON.stringify(fresh), { expirationTtl: 1200 }).catch(() => {}); return fresh; } } After the pin: six requests, one feePayer, purchase settled on-chain in under four seconds. Lesson: treat /supported as coordination state, not as a config fetch. If your facilitator rotates anything and your runtime has more than one instance, challenge and verify must read the same snapshot. 3. Solana blockhashes give you ~90 seconds - slow payment flows are dead flows Symptom: a payment flow that involved carrying the signed payment token between tools (wallet signs in one place, redeem happens in another) always came back BlockhashNotFound . Solana transactions reference a recent blockhash and expire after roughly 60-90 seconds. An EVM payment authorization is a signature over typed data - it keeps for the whole maxTimeoutSeconds window, so relaxed flows (sign here, paste there, redeem later) work. The Solana equivalent embeds a blockhash, so the wall-clock budget from "402 received" to "redeem submitted" is about a minute, regardless of what the x402 timeout field says. Fix: make the client one process. Fetch โ receive 402 โ sign โ retry, no humans and no copy-paste in the loop: import { wrapFetchWithPayment, x402Client } from "@x402/fetch"; import { registerExactSvmScheme } from "@x402/svm/exact/client"; const client = new x402Client(); registerExactSvmScheme(client, { signer }); // @solana/kit signer const payFetch = wrapFetchWithPayment(fetch, client); const res = await payFetch("https://whodeployed.wtf/v1/leaderboard"); // 402 โ sign โ settle โ 200, ~2-4s total, receipt in PAYMENT-RESPONSE Lesson: design Solana x402 clients for a 60-second total budget. Any architecture where the token rests - approval queues, human-in-the-loop wallets, multi-tool relays - needs the signing window to open after the human decision, not before. Lightning round - Bazaar indexing rides on settlements, not registrations. The CDP Bazaar catalogs your endpoint when a compliant settlement carries the discovery declaration ( declareDiscoveryExtension in your route config, echoed by the client). Wallet gateways may strip it - my endpoints only indexed after buys from a spec-compliant@x402/fetch client. - workers.dev bot protection 403s unusual clients. Cloudflare's untunable browser-integrity check onworkers.dev rejects exotic user agents (error 1010) - including some agent frameworks. A custom domain puts those protections under your control. Related: the moment you addroutes towrangler.jsonc , workers.dev silently turns off unless you set"workers_dev": true . - Verify with the right Accept header. My landing serves HTML to browsers and JSON to everything else; I spent ten minutes "debugging" a deploy becausecurl withoutAccept: text/html was grepping the JSON representation. The stack, for reference Cloudflare Worker + Hono, @x402/hono v2 middleware, EVM + SVM schemes registered from one shared resource server, KV-cached snapshots, paid MCP tools on the same Worker (/mcp ), CDP facilitator โ official Bazaar. Total revenue at time of writing: cents - this is an experiment in agent commerce, not a business plan. But every failure above cost hours, and each fix is three lines once you know it exists. The store this came from: whodeployed.wtf - 780+ paper-trading bots' telemetry (Kalshi/Polymarket/crypto/equities), every response self-flagging its staleness. It's also a deliberately cheap, always-on target if you're testing an x402 v2 client: $0.01 gets you real JSON and a settlement receipt; free dry run at /v1/arena . Not investment advice - the data is simulated paper dollars from a research lab that tells you when it's lying. Top comments (0)
Comments
No comments yet. Start the discussion.