DEV Community

Mickael Lamare
Mickael Lamare

Posted on • Originally published at feranor.com

What Shipping a Paid MCP Server Taught Me About Ownership

A small, honest system

A few weeks ago I put a paid tool on the public internet. It scores how resilient a web domain is from the outside (TLS, security headers, DNS redundancy, response time) and it charges $0.01 per call in USDC on Base, settled through x402. No signup, no API key, no dashboard. An agent calls a tool, gets a 402 with the price attached, pays, gets its report.

I did not build this to sell resilience scores. I build governance systems for a living, and I wanted something small enough to hold in my head completely, that would still force every hard question a distributed system asks: who owns this state, what happens when this call fails halfway, what does this component promise and does the code actually keep that promise. A toy that couldn't lie to me.

It didn't. Over one very long week it produced five distinct production failures, each one invisible to every automated test I had written for the feature that caused it. None of them were exotic. All of them were the same failure, wearing a different costume each time.

The pattern: nothing owned its own contract

The first bug looked like a networking problem. A response header advertised 20,335 bytes; the body, after passing through an HTML minifier further down the pipeline, delivered 16,804. Any client that didn't happen to request compression sat there waiting for bytes that would never arrive, while the server reported 200 OK. The header and the body had drifted apart because two different middlewares each believed they owned the response, and neither checked with the other.

The second was worse in a quieter way. A rate limiter correctly rejected excess traffic with 429, and then a generic error-page middleware, further down the same pipeline, re-executed the request and silently turned that 429 into a 400. A 429 tells an agent to wait and retry. A 400 tells it the request was malformed, so it stops trying. The rate limiter thought it owned the response. It didn't; the error handler downstream did, and it disagreed without telling anyone.

The fifth was the one that stung the most, because I found it myself, by accident, reading a tool description in a client I hadn't touched in days. Every call to tools/list appended the price to the tool's description. A small, reasonable-looking string concatenation. Except the SDK reuses the same in-memory object across calls. The description didn't get set; it got appended, forever. After enough calls it was several kilobytes of the same sentence repeated end to end. Nobody owned the canonical description. Every request thought it was allowed to mutate it.

Every one of these bugs was a component acting on state it didn't actually own, discovered only by a test that watched the full round trip instead of one function's return value.

The other two followed the same shape. A payment gate that read HTTP headers set by a caching middleware to decide whether to charge someone, once removed for an unrelated reason, silently stopped the gate from ever emitting a 402 at all. And an idempotency store meant to protect against double-charging on retry, built correctly, tested correctly in isolation, and then handed the wrong cache key by a test harness that used a fixed nonce where it needed a fresh one. Passing for the wrong reason for two days before anyone noticed the assertion wasn't testing what its name said.

This is what I mean when I say ownership is the load-bearing concept in distributed systems, not a slide in a governance deck. Every one of these failures had a component that was, technically, doing its job (minifying HTML, handling errors gracefully, caching, saving a network round trip, retrying a flaky call). Each one was locally correct and globally wrong, because nothing in the system had final, checkable authority over the thing that broke: the byte count in the response, the status code that left the building, the string in a tool description, the header a payment decision depended on, the identity of a single financial transaction.

What actually caught them

Not unit tests. Every one of these five bugs shipped past unit tests that were, individually, correct. What caught them was a small set of tests that exercised the full path a real client takes and inspected the actual bytes on the wire. Not a mocked response, not a function's return value, the literal payload a byte inspector would show you.

  • A contract test that hexdumps the tail of an SSE response and checks for the exact terminator sequence, not just a 200 status code
  • A concurrency test that fires the same payment nonce at the server from sixteen threads at once, because a race that never shows up on a single request is still a race
  • A test that calls tools/list three times in a row and asserts the description is byte-identical each time. The only thing standing between 'it works' and 'it works until someone calls it twice'
  • A test that deliberately tampers with one field of an otherwise valid payment offer and asserts the facilitator is never even called, proving a rejection is cheap, not just that it happens
  • A smoke script that runs against the real deployment after every release, with actual timeouts, because a test that can hang forever is not a test, it's a liability with a green checkmark

None of this is exotic testing theory. It's the same principle that runs through the governance work I do with engineering teams: define who owns a boundary, then write something (a test, a contract, a runbook) that actually checks the boundary holds, instead of trusting that it does because the code around it looks reasonable.

The part that surprised me

I expected the hard part to be x402 itself, like signing payloads, settling on-chain, getting the payment envelope right. It wasn't. The facilitator did exactly what its documentation said it would do, every time. The hard part was everywhere else: the ordinary machinery of a web server (compression, caching, error handling, rate limiting) none of it built with payment logic in mind, all of it sitting in the same request pipeline as payment logic, each piece making a locally sensible decision that broke a promise made three files away.

That's the whole thesis, and it's the same one whether the system moves money or moves inventory or moves configuration across a fleet of services. Complexity doesn't usually announce itself as a hard problem. It shows up as five small, reasonable-looking pieces of code, each one correct on its own, none of them talking to each other about who's actually in charge.


I’m writing a book about this — how distributed systems lose ownership, and the governance framework I built to prevent it.

Read the preface free →

Top comments (0)