blog.shveik.dev

Published ยท 6 min read

x402 vs MPP: what we learned building both

We run small pay-per-call services that accept both x402 and MPP. We settle them in one handler, because both credentials are an EIP-3009 transferWithAuthorization signature on USDC. The headers, the nonce rule, and the binding differ. The signature does not. This post covers what the two protocols have in common, what bit us, and what our traffic says about them.

What x402 and MPP are

Both build on HTTP 402. The client requests a resource, the server answers 402 with a challenge, the client sends a payment credential, and the server settles it and then returns the resource. The specs are the x402 spec and the MPP spec.

x402 puts the challenge in PAYMENT-REQUIRED as base64 JSON, the credential comes back in PAYMENT-SIGNATURE, and success is PAYMENT-RESPONSE.

MPP puts the challenge in WWW-Authenticate: Payment, the credential is Authorization: Payment plus a base64url payload, and success is Payment-Receipt.

A WorkOS comparison says x402 is stateless per request, and that MPP adds sessions: pre-authorized spend, streamed payments, and batch settlement. It also says MPP has been submitted to the IETF, that MPP carries a Stripe and Tempo dependency, and that MPP can express an x402-style one-shot payment.

A TRM Labs analysis, as reported, found only roughly 0.6 to 7.5 percent of screened x402 volume plausibly agentic. Artemis, as reported, estimated about half of x402 transactions were artificial. Coverage of BlackRock's machine-native economy paper says the paper discussed x402 and stablecoins. Fortune reported that Cloudflare announced tooling to charge agents in USDC.

What we built

One handler settles both. Both credentials are an EIP-3009 transferWithAuthorization signature on USDC.

One handler, two protocols A client on the left, one handler in the middle, and USDC settlement on the right. The handler dispatches on the Authorization header. x402 uses PAYMENT-REQUIRED, PAYMENT-SIGNATURE, and PAYMENT-RESPONSE. MPP uses WWW-Authenticate: Payment, Authorization: Payment, and Payment-Receipt. The 402 challenge goes back to the client and the credential comes in. Delivery waits until settlement is confirmed. Ambiguous means not delivered. client one handler verify, settle, then deliver USDC transferWith Authorization dispatch on the Authorization header x402 402 challenge PAYMENT-REQUIRED credential PAYMENT-SIGNATURE success PAYMENT-RESPONSE MPP 402 challenge WWW-Authenticate: Payment credential Authorization: Payment success Payment-Receipt deliver only after settlement is confirmed; ambiguous = not delivered
One handler takes either credential, settles the USDC transferWithAuthorization, and delivers only after settlement is confirmed.

The server tells them apart by the Authorization header. If the first token is Payment, the request is MPP. Otherwise the server looks at the x402 headers.

Each route is served under /x402/<name> and /mpp/<name>. Both paths accept either credential.

The idempotency key we use is:

protocol:eip3009:network:asset:payer:nonce

In our implementation MPP is Base only. x402 also runs on Polygon and Solana. Settlement for Base x402 goes through a facilitator (Coinbase CDP). MPP, and x402 on the other networks, we settle ourselves by sending the transferWithAuthorization transaction.

This is the comparison from our spec.

Credential, nonce, and binding differences between x402 and MPP
Fieldx402MPP
Challenge headerPAYMENT-REQUIRED (base64 JSON)WWW-Authenticate: Payment
Credential headerPAYMENT-SIGNATUREAuthorization: Payment plus base64url
Success headerPAYMENT-RESPONSEPayment-Receipt
EIP-3009 nonceClient-chosen random 32 bytesMust be keccak256 of the challenge id and the realm, binding the payment to one challenge
Challenge integrityNone. The server recomputes the price every timeHMAC over the challenge fields
Body bindingNoneContent-Digest of the request body, bound into the HMAC, so a credential cannot be replayed against a different query

x402 leaves the nonce as 32 random bytes chosen by the client. It does not bind the challenge. It does not bind the body. MPP requires the nonce to be keccak256 of the challenge id and the realm, so the payment is bound to one challenge. Its HMAC covers the challenge fields. The same HMAC covers a Content-Digest of the request body, so a credential cannot be replayed against a different query.

Things that bit us

A facilitator timeout during settle leaves the outcome ambiguous. The authorization nonce was never used on chain, nothing was charged, and a retry works. The server must not deliver until settlement is confirmed. Ambiguous means not delivered.

An overflowing validAfter value passed a naive check but could never settle. That is a free-scrape attack. Numbers must be strict canonical decimals.

The price must be recomputed server side on every request.

An empty or failed result must not be charged.

What the traffic says

In our traffic, about 96 of every 100 payment challenges our servers issued were on /x402/ paths and about 4 were on /mpp/ paths, about 22 to 1. Settled payments split the same way. In absolute terms, MPP payments were a handful.

x402 vs MPP in our traffic Two horizontal bars of relative shares. Payment challenges by path are about 96 percent x402 and 4 percent MPP. Settled payments are about 96 percent x402 and 4 percent MPP. payment challenges by path x402 96% MPP 4% settled payments x402 96% MPP 4% x402 MPP
relative shares, small sample, includes our own tests; path share partly measures discoverability.

Per challenge issued, the share that ended in a paid call was about the same for both, roughly 0.3 percent. MPP's gap is mostly in how many callers arrive, not in whether they pay once they do.

About 94 of 100 payment attempts were on Base. Polygon and Solana together were a few percent.

Why MPP lags

We do not know why fewer callers show up for MPP. These are guesses.

  • Discovery catalogs and scanners mostly index x402.
  • Client libraries and wallets ship x402 first.
  • The first-party tooling most agents use already speaks x402.
  • MPP's headline feature is sessions. Sessions matter for streaming or per-token billing, which a pay-per-call service does not need. WorkOS describes them as pre-authorized spend, streamed payments, and batch settlement.
  • The Stripe and Tempo coupling. WorkOS says MPP carries that dependency.

What we would do as a seller

For a pay-per-call service we would ship x402 first. We would keep the MPP one-shot path in the same handler. The credential is the same transferWithAuthorization on USDC. Both paths already accept either credential. One charge per call does not need a session.

We would list the /x402/ routes where the x402 discovery catalog and the scanners look. We would list the /mpp/ routes wherever MPP is indexed.

Base x402 would keep going through the facilitator (Coinbase CDP). MPP, and x402 on Polygon and Solana, we would keep settling ourselves by sending the transaction.

If we later billed per token, or charged during a stream, we would look at MPP sessions then. That is new work.

Caveats

The sample is small and includes our own test purchases and automated checkers. Path is not protocol: both /x402/<name> and /mpp/<name> accept either credential, so a path hit is not the protocol the client chose. The x402 paths are listed in the discovery catalog and by scanners, the /mpp/ paths are listed in fewer places, and we have not measured that bias, so we call it likely. Path share partly measures discoverability. We built MPP's one-shot charge intent, the EVM charge flow, not its sessions. The TRM Labs, Artemis, WorkOS, BlackRock, and Cloudflare claims below are theirs.

Sources