DEV Community

Amfibi
Amfibi

Posted on AI-assisted

What happened when an agent paid for storage with no account (x402 in practice)

I built a small service for AI agents: store a JSON blob or a file, get it back later, or hand it to a human with a link. Nothing new there. The part I wanted to test was the payment model: no account, no API key, no credit card. The agent pays $0.01 in USDC per call, by itself, using the x402 protocol. This post is what that took, and what broke.

The flow

  1. The agent calls POST /v1/items with the content.
  2. The API answers 402 Payment Required with a small JSON body: amount (10000 units = $0.01 USDC), asset (USDC on Base), recipient, and a timeout.
  3. The agent's x402 client signs an EIP-3009 transferWithAuthorization for exactly that amount and retries the request with the signature in a header.
  4. The API asks a facilitator (Coinbase's) to verify and settle. The facilitator broadcasts the transfer and pays the gas.
  5. The API stores the item and returns 201 with an id, a one-time secret, and links.

The agent needs a wallet with USDC and nothing else. No ETH for gas. No sign-up anywhere.

What broke

The public testnet facilitator is flaky. "Payment settlement failed: Missing or invalid parameters" and "replacement transaction underpriced" show up intermittently; its hot wallet collides on nonces. The fix on our side: treat a failed settlement as "nothing happened" (delete the half-written item, return 402, let the client sign a fresh payment). On-chain reconciliation confirmed a failed settlement never moved money. Production uses the CDP facilitator, which has been clean so far.

Cloudflare Workers caps PBKDF2 at 100,000 iterations. Our password hashing used 600,000; local dev (workerd) does not enforce the cap, production does. The symptom was a 500 only in production on PUT /password.

A cached promise that never settles. We cached the facilitator initialization promise at module level. When the first request was canceled mid-initialization, the pending promise stayed cached forever and every later request hung. Cache only initialized objects, never pending promises.

Concurrent read counting on object storage. R2 has conditional writes but no atomic decrement. 100 back-to-back reads lost 26–44 decrements. Backoff with jitter brought it down to a handful; we documented the counter as soft and moved on. A Durable Object would make it exact; it was not worth it at $0.01 per 1,000 reads.

The client consumed the request body on retry. The x402 fetch wrapper replays the request after the 402; a body stream can only be read once. Clone before the first attempt.

What I would tell someone doing the same

  • Put a one-shot smoke test with real money in the repo. Ours makes one upload, verifies the receipt and the single USDC Transfer on chain, then deletes the item. It found a config regression that unit tests could not.
  • Write the audit trail yourself. Logpush needs a paid plan; a background put per settlement into a separate bucket, rolled up daily, was 100 lines and works on the free plan.
  • Agents do not discover services on their own yet. They find them through developers. An MCP server (npx -y fileshareforagents-mcp), llms.txt, and being in the x402 Bazaar matter more than any feature.
  • Terms matter even at $0.01. A drop box that deletes content at expiry needs to say so in plain words before anyone pays.

Numbers so far

Two full end-to-end runs on mainnet (36 steps each, 10 settlements per run), every settlement reconciled on chain, zero money moved by failed settlements. Price: $0.01 per store, extend, or 1,000 reads. Everything else free.

Try it on testnet with free USDC: https://fileshareforagents.online. Docs for agents: https://fileshareforagents.online/llms.txt. I read every message sent to POST /v1/feedback.

Top comments (0)