Part of a series on building cz-agents → under the hood
Where we left off
A few weeks ago I wrote that all the x402 economy ever sent to my public MCP servers were probes — indexing services mapping who can accept a machine payment. No agent with a wallet, no demand, no coin. And I was blunt about why: at that point my servers did not accept x402 at all. Zero payments on a server with no payment rail is not a data point about the protocol; it is a data point about my priorities.
But that sentence would not let me rest. Saying "I have no payment rail" is one thing; knowing whether that rail, once built, could actually carry a real coin is another. I wanted to close that gap — not for customers who are not there yet, but for myself: to know whether I was talking about something that works, or a conference slide.
The milestone: the coin moved
So I took x402 all the way. And at the end, exactly the thing I had asked about happened: real USDC settled on Base mainnet in an on-chain transaction. Not test tokens on a testnet, not a simulation — an actual stablecoin that moved from one side to the other, confirmed by the chain, end-to-end. The rail carries the coin. That is the answer to the question I posed last time.
And now the thing I have to say up front, so the reader does not fill it in and be right to: it was a controlled demonstration, a canary, not a paying customer. I controlled one side of that transaction and the other. It was a live test — real money, real chain — but not a commercial validation, and not an organic customer who showed up on their own wanting to buy something. That is the difference between "I can accept a payment" and "someone is paying me," and that difference is the whole rest of this trade.
The milestone is real, then, but modest. Something changed in engineering terms: there is now a verified path a real coin can travel. Nothing changed in business terms: customers still are not coming. Both are true at once, and honesty means saying both halves out loud, not just the one that quotes better.
Four hooks a live run found, and tests did not
More valuable than the coin, though, is what broke along the way. Because something showed up that I know from other domains but which stings more with money involved: a class of bugs never surfaces in tests or on the testnet — only against a real counterparty in a live run. All four hooks below would pass unit tests. I write them as portable traps of the x402 and EVM ecosystem, not as a description of my system — because these are exactly the things that will trip anyone building on the same foundation.
1. The EIP-712 signing domain differs between testnet and mainnet
A payment here is a signed message, and the signature is bound to what is called a signing domain — a set of values that unambiguously state what is being signed and where. One of those values is the token's name. Here is the trap: on the testnet, USDC calls itself USDC in that domain, whereas on mainnet it is USD Coin. Hard-code the value from what you saw on the testnet, and on mainnet you produce an invalid signature on every single payment — and verification rejects it. The nasty part is that the testnet never catches this: there, the value is correct. A thousand green tests pass through, and the first live mainnet transaction fails.
2. One payload field is an object, not a string
In the data the client sends, there is a field that looks like a simple value but is in fact a structured object. The client returns it unchanged, exactly as it should. The problem is on the checking side: when it compares the object as if it were a string, it never matches — and when it does not match, nothing is issued. The check fails silently and the response is never sent. No error, no exception, just emptiness where the data should have been.
3. The signing domain must be handed to the client explicitly
Related to the first point, but a trap of its own. The client has nowhere to get the signing domain from unless the server explicitly hands it over. Forget to, and the client has nothing to sign with — and what it does not sign, the other side cannot verify. In a test where you hold both ends yourself, that value tends to "just be there," because you supply it by hand. In a live run with a foreign client it is missing, and the whole flow stalls at the start.
4. The expiry check can be impossible to satisfy
The last one is the most insidious, because it looks like strictness and is actually a logical trap. A payment's validity check may require that, at the moment of verification, the full time window still remains. But the client sets that window to exactly that value. The result: the condition is satisfiable only at zero elapsed time — only if verification ran in the very instant the client signed, which in reality never happens. Every real payment arrives a fraction of a second later and is rejected as expired. Tests miss it when you route the timing around the condition yourself.
The meta-point across all four: each passes unit tests and only surfaces against a real counterparty in a live run. The testnet, moreover, hides some things entirely — a testnet facilitator, for instance, tends to run without authentication, whereas mainnet requires a verified account. Which means "it works end-to-end on the testnet" is a label with an asterisk too. You passed a simplified version of reality.
Why these bugs live right here
It is no accident that none of those four things showed up earlier. They all live in the gap between "it passed the tests and works on the testnet" and "it works with real money on mainnet." That gap is narrow, easy to miss — and it is exactly where the difference breaks between what you think your system does and what it actually does.
A unit test verifies that the code behaves according to your mental model. But three of those four hooks are cases where the mental model itself is wrong: a hard-coded testnet value, comparing an object as a string, an impossible time condition. A test built on the same mistaken model confirms it, does not refute it — it goes green and lies about everything. The bug is exposed only by a counterparty that does not share your model, because it runs by the protocol, not by your expectation.
With money, the cost of that mistake is concrete and unpleasant. It is not a red test in CI that gets fixed in the next commit. It is burnt gas on a transaction that went nowhere, and at the other end a counterparty that paid and got nothing — or, the reverse, got something and did not pay. That is precisely why this layer is not verified by "it passed," but by an actual coin actually going through. Nothing less closes the gap.
What I take from it
The answer to the question I posed last time — can the rail carry a real coin? — is yes, verified. Real USDC settled on Base mainnet, end-to-end. That is a done thing, not a slide.
But I will frame it as it is, not as it would share better. The x402 ecosystem is still very early: across the protocol, July brought almost no organic payments, the overwhelming majority of settlements were tests, and that is especially true here in Europe. My coin belongs in that statistic — it was a demonstration, not a customer. The milestone I closed is an engineering one: the rail carries the coin. The milestone that is not closed is the business one: someone showing up on their own to send a coin down that rail. That second one is still waiting, and it is fine to say so out loud.
What I take away most, in the end, is not the money — it is those four hooks. They reminded me of something easy to forget when you have green tests and a working testnet: "it passed" and "it works with real money" are two different claims, and between them sits exactly as much room as the unexpected bugs need. The milestone is modest. The lesson is not.
Trying to tell whether an integration in your company really carries live traffic, not just tests? → AI consulting