cd /news/ai-agents/self-hosting-an-x402-facilitator-the… · home topics ai-agents article
[ARTICLE · art-52216] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Self-hosting an x402 facilitator: the EIP-3009 gotchas that almost cost us money

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.

read3 min views1 publishedJul 9, 2026

We run DeskCrew, a helpdesk where AI agents can use support tools with no account and no API key — they pay per action in USDC over x402, the HTTP-402 payment standard. An agent calls a tool, gets a 402 Payment Required

with an exact quote, signs a USDC authorization, retries, done. Five cents, no signup.

Most 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.

402

with a quote: price in atomic USDC, your receiving address, network, a timeout.transferWithAuthorization

— a gasless, off-chain signature authorizing a USDC transfer from their wallet to yours, bound to a unique nonce.X-PAYMENT

header.Simple. The gotchas live in steps 2–4.

This is the one that matters. Our first settlement-recovery path checked authorizationState(from, nonce)

on the USDC contract — if the nonce was used, we marked the payment settled and the revenue earned.

The payer controls that nonce. EIP-3009 lets the authorizer burn their own nonce: they can call cancelAuthorization

, 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.

The fix is to confirm the money movement, not the nonce state. On settlement recovery we now require finding the AuthorizationUsed(authorizer, nonce)

event and verifying the USDC Transfer

log adjacent to it in the same transaction — checking it moved from

the payer, to

our receiving wallet, with value >= the 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.

Rule of thumb: any state the payer can influence is not proof of payment. Only the Transfer to your address is.

Two invariants pull against each other:

Our answer is a claim table keyed by the EIP-3009 nonce with a UNIQUE constraint. The first request to insert the pending

row 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.

The 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.

"USDC on five chains" sounds like a for-loop. It isn't:

If you're starting out: ship Base only, add chains when someone asks. We serve /.well-known/x402

, 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.

The door is live on mainnet:

https://deskcrew.io/.well-known/x402 draft_support_reply

at $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.

Questions about the facilitator internals welcome — happy to go deeper on any of the four gotchas.

── more in #ai-agents 4 stories · sorted by recency
── more on @deskcrew 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/self-hosting-an-x402…] indexed:0 read:3min 2026-07-09 ·