AI agents are starting to pay for APIs on their own. On Stellar, two protocols make that possible: x402, for one-off paid requests, and MPP, the Machine Payments Protocol, which adds payment channels for agents that call the same API many times. In channel mode the agent signs off-chain commitments instead of paying on-chain every time, and the server has to refuse any commitment that is stale or replayed. Get that wrong and a service can be paid twice for the same money, or not at all.
I built Wasit, an open-source tester that checks x402 and MPP services against the spec from the outside, as a client would. It runs the real payment flow on Stellar testnet, and for channel mode it deliberately sends a stale commitment, a replayed credential and a replayed commitment, and expects each to be refused with HTTP 402.
What happened
On 24 September I ran Wasit against the official MPP SDK's own example servers, unmodified. The channel checks failed. The server was still refusing every bad commitment, so nothing was paid twice, but every refusal came back as HTTP 500 "internal payment error" instead of 402.
To find out why, I ran the same checks against two commits: the one before a dependency upgrade merged two days earlier, and the one after.
SDK commit Channel checks Refusal status
Before the upgrade Pass 402
After the upgrade Fail 500
The upgrade had changed how the payment framework wraps errors it does not recognise, and the SDK's own error classes were not the kind it recognised.
Why a status code matters
A 402 tells an agent "your payment was wrong, here is a fresh challenge". A 500 tells it "the server is broken". Retry logic, monitoring and the agent's own decisions all branch on that difference. An agent that sent a stale amount would have seen what looked like an outage.
The fix
I filed it upstream the same day with the A/B above. Four days later the maintainers merged a fix, and added live testnet integration tests to their CI, because mocked tests alone had let this through. I re-ran Wasit against the fixed commit: every check passed again, and every refusal was back to 402. The regression never reached a release.
What it taught me about Wasit
Running Wasit against code I didn't write found problems in Wasit too. In this run, two of its checks called the 500 a "double-spend", which overstated what had happened, since the server had refused. That wording is fixed in 0.5.0. A conformance tester that exaggerates is worse than none.
Try it
npx -y @wasit-dev/cli test --target <your paid URL> --read-only
The read-only checks cost nothing. Wasit is testnet-only, and you should only point it at services you run or have permission to test.
Github
The issue and fix: stellar/stellar-mpp-sdk#82 and #83
upstream
Findings
Top comments (0)