{"slug": "self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money", "title": "Self-hosting an x402 facilitator: the EIP-3009 gotchas that almost cost us money", "summary": "DeskCrew, a helpdesk for AI agents, self-hosted the x402 payment facilitator stack and encountered critical issues with EIP-3009 nonce handling. The team found that checking only the nonce state was insufficient because the payer can cancel or reuse it, leading to potential revenue loss. They implemented a fix that verifies the actual USDC Transfer event instead, and also designed a claim table with a UNIQUE constraint on the nonce to prevent replay attacks.", "body_md": "We run [DeskCrew](https://deskcrew.io), a helpdesk where AI agents can use support tools with no account and no API key — they pay per action in USDC over [x402](https://www.x402.org), the HTTP-402 payment standard. An agent calls a tool, gets a `402 Payment Required`\n\nwith an exact quote, signs a USDC authorization, retries, done. Five cents, no signup.\n\nMost x402 services use a hosted facilitator to verify and settle payments. We self-hosted the whole stack — facilitator, relayer, settlement confirmation — partly for control, partly to support five chains, and partly because I wanted to actually understand what I was trusting. Here's what bit us on the way, so it doesn't bite you.\n\n`402`\n\nwith a quote: price in atomic USDC, your receiving address, network, a timeout.`transferWithAuthorization`\n\n— a gasless, off-chain signature authorizing a USDC transfer from their wallet to yours, bound to a unique nonce.`X-PAYMENT`\n\nheader.Simple. The gotchas live in steps 2–4.\n\nThis is the one that matters. Our first settlement-recovery path checked `authorizationState(from, nonce)`\n\non the USDC contract — if the nonce was used, we marked the payment settled and the revenue earned.\n\n**The payer controls that nonce.** EIP-3009 lets the *authorizer* burn their own nonce: they can call `cancelAuthorization`\n\n, or spend the same nonce in a self-transfer, racing your relayer. Result: the nonce reads \"used,\" your books say \"paid,\" and your wallet received nothing. An adversarial agent could get your tool's output for free, at scale.\n\nThe fix is to confirm the *money movement*, not the nonce state. On settlement recovery we now require finding the `AuthorizationUsed(authorizer, nonce)`\n\nevent **and** verifying the USDC `Transfer`\n\nlog adjacent to it in the same transaction — checking it moved `from`\n\nthe payer, `to`\n\nour receiving wallet, with `value >=`\n\nthe quoted amount. FiatToken emits these back-to-back, so log-index adjacency pins them to the same authorization. Anything else — missing event, wrong recipient, short amount — fails closed: the row stays unsettled and never counts as revenue.\n\nRule of thumb: **any state the payer can influence is not proof of payment. Only the Transfer to your address is.**\n\nTwo invariants pull against each other:\n\nOur answer is a claim table keyed by the EIP-3009 nonce with a UNIQUE constraint. The first request to insert the `pending`\n\nrow wins the race and runs the tool; everyone else carrying the same nonce is a retry and gets refused. If the tool succeeds but settlement fails (RPC hiccup, gas spike), we stamp the row consumed-but-unsettled — the row is *never deleted*, so the same authorization can never re-run the action — and a reconcile job retries the on-chain settlement later, using the Gotcha-1 confirmation before it ever promotes the row to revenue.\n\nThe ordering that emerges: **claim the nonce → validate input → run the tool → settle → record.** Validation before payment matters more than it looks — taking five cents and then returning \"invalid input\" is a great way to make an agent developer never come back.\n\n\"USDC on five chains\" sounds like a for-loop. It isn't:\n\nIf you're starting out: ship Base only, add chains when someone asks.\n\nWe serve `/.well-known/x402`\n\n, per-workspace manifests, and we're in the MCP registry. Do all of that — it's cheap and it's how agents *can* find you. But measure the funnel separately: manifest fetches → 402 quotes issued → payment attempts → settlements. Fetches without quotes are curiosity. Quotes without attempts are checkout friction. Only settled transfers from wallets you don't control are demand. We log all four stages and split \"our own test wallets\" from strangers, so we can't fool ourselves.\n\nThe door is live on mainnet:\n\n`https://deskcrew.io/.well-known/x402`\n\n`draft_support_reply`\n\nat $0.05 — send raw customer text, get a support reply draft back. Payment settles only after the tool succeeds.If you've got an x402-capable agent: the first 20 external wallets to make a successful paid call get **5× the call price back** — reply with your tx hash wherever you found this post. Worst case, you're out two cents and got a story.\n\nQuestions about the facilitator internals welcome — happy to go deeper on any of the four gotchas.", "url": "https://wpnews.pro/news/self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money", "canonical_source": "https://dev.to/linknpark/elf-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money-443k", "published_at": "2026-07-09 07:30:28+00:00", "updated_at": "2026-07-09 07:41:19.931532+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["DeskCrew", "EIP-3009", "USDC", "x402", "Base"], "alternates": {"html": "https://wpnews.pro/news/self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money", "markdown": "https://wpnews.pro/news/self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money.md", "text": "https://wpnews.pro/news/self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money.txt", "jsonld": "https://wpnews.pro/news/self-hosting-an-x402-facilitator-the-eip-3009-gotchas-that-almost-cost-us-money.jsonld"}}