DEV Community

holistis
holistis

Posted on

x402 payments prove money moved. They don't prove you got what you paid for.

x402 payments prove money moved. They prove nothing about whether you got what you paid for.

That's the gap I ran into while building agent-to-agent infrastructure. x402 solves something real: one machine can pay another machine, per request, no bank, no human clicking approve. What it doesn't solve is delivery. The blockchain is the receipt for the payment. It says nothing about the quality, quantity, or duration of what was supposed to come back.

That gap is easy to miss for a single API call, you got a response or you didn't. It's much harder to ignore for metered or duration-based resources: GPU-hours, storage over a month, bandwidth over a period. Those aren't one moment you can check. They're a claim about quantity and duration that only becomes verifiable after the fact, by which point the seller can be long gone.

What I built

capacity-attest is a small, open-source MCP server that addresses this specific gap. After an x402 payment settles, the paying agent signs a plain factual record with its own key: what was promised, what was delivered (yes / no / partial), a hash of the evidence, and the settlement reference. That record is appended to a public, per-seller, append-only history that any future buyer can check before paying that seller.

No reputation score. No ranking. No "trusted seller" badge. Just a signed fact: this is what was claimed to be delivered, and here is proof the buyer signed off on that claim themselves.

I went with no score deliberately. A score needs someone to compute it, and whoever computes it becomes the thing worth attacking, manipulate the score instead of the underlying service. A plain signed record sidesteps that question entirely. It doesn't say "this seller is good." It says "here is exactly what was claimed, signed by the party who actually received it."

Today's proof, not a demo

I didn't want to ship this on the strength of unit tests against a mock signer. So today I ran it against something real:

  • A real $0.001 USDC payment on Base mainnet, from a fresh wallet, to a live paid API I run.
  • The service responded with real data (200, real JSON).
  • The paying wallet then signed a real delivery claim (EIP-191, over the claim's content-address) and recorded it via capacity-attest.
  • Read the history straight back and it was there, correct, matching the on-chain settlement reference.

Total time from confirmed payment to a readable, signed record: about 5 seconds. The payment confirmation itself was the slow part (4.5s), everything capacity-attest did (sign, record, read back) took about 11ms combined.

The transaction is public if you want to check it yourself: 0xc00a638491986962ca125c01da4e3df8a07d715216694bb0a59ff257c88e6880 on Base.

Getting the boring parts right

Before I trusted this enough to run it against real money, I put it through an adversarial pass against itself: real multi-process concurrency tests, not just unit tests with mocks. That found and fixed a real race condition (duplicate claims under concurrent writes), a crash path (unbounded recursion on a hostile input), a case-sensitivity bug that let a claim dodge its own duplicate-check, and a blocking synchronous lock that could freeze an entire live server for seconds under contention.

Each of those got fixed and then re-tested with the exact attack that found it, not just assumed fixed because the code looked right. One fix for a caching layer even needed three rounds before it held up, and one small, low-severity edge case (a narrow race in an in-flight cache resync, only reachable if a second writer bypasses the lock file entirely) is still open and documented as a known limitation rather than silently patched over or hidden.

I'd rather ship something with an honestly documented small gap than something that looks clean and isn't.

What it deliberately doesn't do

No on-chain anchoring. No ERC-8004 identity binding. No reputation scoring. Those are all legitimate directions to take this, but adding them now would trade away the one property that matters most for something this early: staying simple enough that anyone can read the whole thing and verify it does what it claims.

Status, honestly

This is new. Today was the first time it touched real money and a real live service instead of test keys. There are no external users yet. It's built because the gap is visible in the current x402 ecosystem discussion around delivery receipts, where the existing schema proposals are shaped for single request/response payloads, not metered or ongoing delivery. That's the specific case this fills.

If you're building anything that sells metered or continuous capacity over x402 and have run into this same "did they actually deliver" question, I'd like to hear how you're handling it.

Top comments (0)