A paid request to MPP Router, step by step: 402 challenge, MPP charge vs x402 exact, sessions

The wire flow, not the theory: the real 402 our router returns on Stellar, what the paid retry does, why an agent needs no gas token, and when to open a session instead of paying per call.

8 min readMPP Router team

An agent calling a paid API has an account problem before it has a payment problem. Signup, an API key, and a prepaid balance are three pieces of state a short-lived agent process does not want to own — and the amounts are absurd: one DeepSeek completion through our router quotes at $0.004. Nobody wants an invoice or a dashboard login for four tenths of a cent.

This post is the wire flow instead of the theory: the actual 402 our router returns, what a paid retry does, when to open a session rather than pay per call, and what does not work yet.

New to the two protocols? Start with MPP vs x402 on Stellar, which compares them side by side. This walkthrough assumes you have picked one.

What differs once you are on the wire

Both dialects come back on the same response, so the choice is not about which one our router supports. Who challenges is the same — the resource server, on the first unpaid request. What is signed differs in scope: x402 exact and MPP charge sign one transfer for one fixed price, while MPP channel signs a cumulative watermark covering everything spent so far.

Where settlement is proven differs too, and this is the part worth knowing before you pick. On the x402 leg our router pays the upstream merchant first and broadcasts the agent’s signed Soroban invoke only after the merchant returns 2xx — if the merchant fails we never broadcast and the agent’s funds never move. On the MPP charge leg the inbound payment is confirmed first, and a merchant timeout, 5xx, 403 or empty body creates a full idempotent refund job whose status is served back on the response as Refund-Id / Refund-Status-Url.

Why Stellar makes $0.004 calls tolerable

  • USDC via its Stellar Asset Contract. One asset id, seven decimals, on stellar:pubnet. Because USDC on Stellar has 7 decimals and upstream quotes sometimes have 6, our router converts and refuses a quote whose precision exceeds 7 decimals rather than rounding against the agent.
  • Settlement cost and speed. Stellar closes a ledger in roughly five seconds and a fee is a fraction of a cent, so the payment does not cost more than the call.
  • Sponsored fees. Our router’s challenges carry feePayer: true (MPP dialect) and extra.areFeesSponsored: true (x402 dialect), and GET /x402/supported advertises fees_sponsored: true. A gas sponsor account pays the Stellar transaction fee, so for per-request payments (charge and x402 exact) an agent wallet needs USDC only. Opening a payment channel is different: that is a Soroban transaction the agent submits itself, so budget a little XLM (about 0.5 XLM today) for the one-time open.

The same endpoint also reports "facilitator": "self" — there is no third-party facilitator in the loop; the router verifies and settles.

A real 402, and what a paid request does

This is a live probe run on 2026-09-07:

curl -s -X POST https://apiserver.mpprouter.dev/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}'

It returns HTTP/2 402 with content-type: application/problem+json, a body of {"type":"https://paymentauth.org/problems/payment-required","title":"Payment Required","status":402,"detail":"Payment is required.","challengeId":"<redacted>"} — and two headers carrying the same quote. Decoding the MPP request blob:

{
  "amount": "40000",
  "currency": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
  "methodDetails": {
    "credentialTypes": ["transaction"],
    "feePayer": true,
    "network": "stellar:pubnet"
  },
  "recipient": "GDK3AVW3YE6UL3J4WLNKBMP65KSY32YPUKIOC6PXW65XJ3LEG3YIDXXB"
}

And decoding the payment-required header from the same response:

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": { "url": "https://apiserver.mpprouter.dev/v1/services/deepseek/chat" },
  "accepts": [{
    "scheme": "exact",
    "network": "stellar:pubnet",
    "amount": "40000",
    "asset": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
    "payTo": "GDK3AVW3YE6UL3J4WLNKBMP65KSY32YPUKIOC6PXW65XJ3LEG3YIDXXB",
    "maxTimeoutSeconds": 300,
    "extra": { "areFeesSponsored": true }
  }]
}

The parity rule is visible side by side: 40000 base units (7 decimals) is $0.004 in both, the asset is the same USDC SAC, and recipient equals payTo. Either dialect yields identical economics. The quote lasts five minutes (maxTimeoutSeconds: 300).

To pay, sign a SAC transfer of exactly amount base units to that address and retry the same request with either Authorization: Payment <base64> (mppx convention) or Payment-Signature: <base64> (x402 v2). Both normalize to one verification path, which checks challenge id, amount, asset and recipient before any upstream spend. A 200 carries {"success": true, "data": <upstream body>}; 202 means an async job to poll; 400 is issued before any 402, so a malformed request is never charged for; 502 means the payment or upstream leg failed after intake.

When to open a session instead

Use charge (or x402 exact) for a fixed-price route and a handful of calls. Open a channel when pricing is usage-based — per token, per compute unit — where a fixed per-call quote is impossible, or when one signature per call becomes the bottleneck. An agent advertises channel intent on its first request (?payment=channel&agent=G…), opens the channel with a deposit cap that bounds its total risk, and then sends a signed voucher per call for the new cumulativetotal. The router verifies the watermark is monotonically increasing and refuses anything above the cap. Unspent deposit stays with the agent per the channel contract’s close rules, and the funder can recover an unused balance on chain without contacting the router at all.

One consequence: routes whose upstream is itself session-priced advertise only channel, never charge — precisely to avoid taking a payment the upstream then rejects.

What you get as a buyer

Settled calls land in a public, unauthenticated ledger at GET /v1/ledger — one row per settled call with the service slug, amount, delivery status, upstream status and the Stellar transaction hash, oldest-first and queryable by ?tx=. It is deliberately raw: no aggregates, so nobody has to trust our arithmetic. There is also a buyer dashboard where an operator signs in with their wallet (no email, no API key) and sees their own calls with model, tokens, cost, latency and refund state, plus open sessions. It is non-custodial by design: "top up" means funding your own channel; the router never holds a balance for you.

Honest limitations

  • Base is view-and-login only. GET /x402/supported lists exactly one kind today, stellar:pubnet. There is no router settlement on Base; the Base rail in the buyer dashboard is a planned phase, not shipped behaviour.
  • Groq-backed models: verified for short completions only. Paid probes on 2026-09-07 of openai/gpt-oss-20b and openai/gpt-oss-120b paid and delivered (HTTP 200 with usage). Earlier Groq llama-* routes paid and then failed, so treat other Groq-backed entries as untested until we say otherwise.
  • No streaming. The proxy buffers the upstream response in full, because delivery has to happen after payment; stream: true is rejected before any payment path.
  • Async jobs are not in the public ledger yet, and on the x402 leg the payer field is null because that dialect carries the payer inside a signed envelope the router does not decode at settlement — the settlement transaction still identifies it on chain.
  • The ecosystem is small. SDF markets x402; MPP on Stellar lives mostly in docs and a young SDK. Expect to read source.

Copy-paste

Probe the challenge for a DeepSeek completion through the OpenAI-compatible facade:

curl -si -X POST https://apiserver.mpprouter.dev/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}'

Then decode either header, sign the transfer, and retry the exact same request with your credential attached. GET /v1/models lists what the facade accepts, and GET /.well-known/x402 is the machine-readable manifest of every payable resource. The flow is written up step by step on How it works, and the live catalog is on Services.