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.