Lab 10 — Machine payments: HTTP 402 and the Tempo MPP stack
Question
HTTP 402 was reserved for "Payment Required" since 1991 but never standardized. Tempo's MPP (Machine Payments Protocol) finally builds it out — AI agents pay for API calls per request. Read the MPP stack and understand where machine-to-machine payments are heading.
Principle (minimum model)
- HTTP 402 protocol. Server returns
402 Payment Required+ a payment quote (price + receiver + expiry). Client retries with a signed payment proof in a header (X-Tempo-Payment: <sig>). - Tempo MPP stack. A Tempo L1 chain optimised for sub-cent transactions + an SDK for clients (Rust / TypeScript) + a middleware for servers (Express / Axum).
- Payment receipt. A signed message proving "this user paid $X for endpoint Y at time T". Server verifies the signature on receipt; logs the receipt for audit.
- Settlement. Receipts are batched and settled on-chain periodically. Off-chain receipts are fast (< 100 ms); on-chain settlement is hourly.
- Why agents need this. AI agents call many APIs per task. Per-call subscription is impractical; per-call micropayments via 402 + MPP make agentic workflows economical.
- Adversarial considerations. Replay protection (nonce in receipt) + amount caps + per-merchant signatures + server-side rate limiting.
- Why this is in the Building track. Forecasts the next major surface area for the Rust EVM stack — machine-to-machine payments at scale.
Worked example + steps
Machine payments — HTTP 402 and the Tempo MPP stack
Every paid API in 2026 forces the same dance. Sign up. Verify email. Provision an API key. Wire a billing account. Pre-commit to a plan. Then you can fetch one paid resource. For a human shipping a SaaS, fine. For an autonomous agent that needed one flight-status lookup once, the friction is the product — and the product is broken.
The Machine Payments Protocol (MPP) — IETF draft, jointly developed by Tempo Labs and Stripe — changes the contract. The client makes the HTTP request. The server replies 402 Payment Required with a challenge. The client pays. The client retries with proof. The server returns 200 OK. No account. No key. No checkout flow. Pay-per-request, in the same roundtrip, against any payment rail the server accepts — Tempo, Stripe, ACH, Lightning, card, custom.
This lesson reads the source: the spec (tempoxyz/mpp-specs), the Rust SDK (tempoxyz/mpp-rs), and a working end-to-end CLI (tempoxyz/wallet).
📌 Spec status — hedge appropriately. MPP is an IETF draft (draft-ryan-httpauth-payment-00), not a ratified standard. The wire format may still evolve. What's stable enough to build on right now is the shape — HTTP 402 + a
Paymentauth scheme — and the Tempo/Stripe reference implementations. Treat the bytes as draft; treat the architecture as the lesson.
Acceptance criteria
The lesson is complete when these tests pass (full code at the end in §Test gate):
returns_402_without_payment— a request withoutX-PAYMENTreturns402with the cost + receiving address.returns_resource_with_valid_payment— the same request with a valid micropayment returns200+ the resource body.rejects_replayed_payment— the same payment receipt cannot satisfy two requests; the second returns402or409.
Test-first reading. The walkthrough below shows the 402 challenge format, payment receipt structure, and replay-protection mechanism — the contract these tests pin down.
The status code that nobody used
HTTP 402 was reserved 30 years ago "for future use." It sat unused while the web grew around API keys, OAuth, Stripe Checkout, and every other workaround for not having a native payment status code. MPP is the first serious specification to claim it.
The full flow — reproduced straight from mpp-specs/README.md:
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /resource
Server-->>Client: 402 Payment Required<br/>WWW-Authenticate: Payment ...
Note over Client: Client fulfills payment challenge
Client->>Server: GET /resource<br/>Authorization: Payment credential
Server-->>Client: 200 OK
Five steps. Read the boundaries:
- Client
GET /resource— same request shape as any uncached, unauthenticated GET. - Server
402 Payment Required+WWW-Authenticate: Payment <challenge>— the challenge encodes what payment is needed and which rails the server accepts. - Client fulfills the payment off-band — a Tempo on-chain charge, a Stripe Shared Payment Token, a Lightning invoice. The protocol does not care which.
- Client retries
GET /resourcewithAuthorization: Payment <credential>— the credential proves the payment happened. - Server validates, returns
200 OK.
The piece that earns the design its agnosticism: step 3 is not in the HTTP roundtrip. The protocol specifies the handshake (steps 1, 2, 4, 5) and delegates the settlement to whatever rail the challenge advertised. That separation is the whole game.
Three layers — Core / Intents / Methods
The spec repo is modular for a reason. Open tempoxyz/mpp-specs/specs/. Three subdirectories matter:
| Layer | Path | What it pins down |
|---|---|---|
| Core | specs/core/ | HTTP 402 semantics, the Payment auth scheme, header grammar, IANA registries. Payment-rail agnostic. |
| Intents | specs/intents/ | Abstract patterns: charge, authorize, subscription. Define what kind of payment without specifying how. |
| Methods | specs/methods/ | Concrete per-rail implementations: tempo/, stripe/, evm/, solana/, stellar/, lightning/, card/. |
The design problem the split solves: how do you write one client library that works against Tempo and Stripe and ACH and Lightning? Answer — the same shape as the Collector/Strategy/Executor split you read in the artemis lesson. Separate the HTTP mechanics (Core) from the payment intent (Intents) from the rail-specific bytes (Methods). The Core layer doesn't know what a blockchain is. The Methods layer doesn't know what HTTP 402 looks like. The Intents layer is the contract between them.
If you forced Tempo-specific assumptions into Core, the moment Stripe joined as a co-maintainer — which happened — every piece of Core would need re-litigating. The modular split made adding Stripe a matter of dropping a new directory under specs/methods/stripe/ without touching the protocol itself.
🔍 Find in repo. Open
specs/core/draft-httpauth-payment-00.md. Skim the IANA registry section. In your own words: what does adding a new payment method actually require? Is the answer "a PR against Core" or "a PR adding one file underspecs/methods/"? (The answer is what makes this protocol future-proof — and what makesWWW-Authenticate: Paymentextensible by design.)
The Rust SDK in 30 seconds
Open tempoxyz/mpp-rs. The SDK splits along the same fault line as the protocol — src/server/ for the merchant side, src/client/ for the buyer side.
Server side — produce challenges, verify credentials:
use mpp::server::{Mpp, tempo, TempoConfig};
let mpp = Mpp::create(tempo(TempoConfig {
recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f1B0F2",
}))?;
let challenge = mpp.charge("1")?; // returns WWW-Authenticate value
let receipt = mpp.verify_credential(&credential).await?;
Mpp::create takes a payment provider — tempo(...), stripe(...), your own. The returned Mpp produces challenges and validates credentials generically. Swap the provider; the rest of your server doesn't move.
Client side — make 402s transparent:
use mpp::client::{PaymentMiddleware, TempoProvider};
use reqwest_middleware::ClientBuilder;
let provider = TempoProvider::new(signer, "https://rpc.moderato.tempo.xyz")?;
let client = ClientBuilder::new(reqwest::Client::new())
.with(PaymentMiddleware::new(provider))
.build();
// Requests now handle 402 automatically
let resp = client.get("https://mpp.dev/api/ping/paid").send().await?;
PaymentMiddleware wraps a reqwest client. The middleware intercepts 402 responses, parses the challenge, calls the provider to fulfill the payment, then retries with the Authorization: Payment header. For callers, .get(...).send() works transparently against any MPP-compatible endpoint.
🔍 Find in repo. Open
src/client/middleware.rsand find the function that handles the retry. Then opensrc/server/mpp.rsand find where the challenge is emitted. Predict: what's the smallest change you'd make to add a new payment provider — say, a custom L2's native asset? (Answer: implement thePaymentProvidertrait on the client side and theChargeMethodtrait on the server. Zero changes to middleware or the protocol parser.)
Why Intents matter — the agent-at-scale problem
(Answer: 1000 on-chain charges are slow, expensive in fees, and serialize the agent against block time. The Intents layer separates charge — settle each request individually — from authorize and subscription patterns — open a channel or session once, settle later. The wallet's "Session Payment (Channel)" mode below is exactly this — an on-chain channel opens once, off-chain vouchers settle per request, the channel closes when you're done. The Core layer doesn't know about channels. It knows about challenges and credentials. The Intents layer is where "1000 cheap requests per session" gets a name.)
The relevant directories: specs/intents/draft-payment-intent-charge-00.md is the one ratified intent at draft stage. Authorize and subscription are on the roadmap per the README.
From protocol to production — tempo wallet
tempoxyz/wallet is the canonical working integration. A CLI wallet with MPP built in. Three commands carry the whole flow:
tempo wallet login # passkey login, opens browser
tempo wallet fund # top up
tempo request https://aviationstack.mpp.tempo.xyz/v1/flights?flight_iata=AA100
The third command is the lesson. tempo request <url> performs the full 402 dance — challenge, sign, pay, retry — and prints the response body. No API key. No billing account. Just the wallet exists and is funded.
Two payment shapes the wallet supports, lifted straight from the README:
| Shape | Trade-off | Use when |
|---|---|---|
| One-shot (charge) | Every request independently settles on-chain. No session state. | Sporadic paid calls, low frequency. |
| Session (channel) | On-chain channel opens once. Off-chain vouchers exchanged per request. Settles when you close. | Streaming (SSE token-by-token), repeated calls to the same endpoint. |
Session mode is the Intents-layer answer to the 1000-requests-per-minute predict above, made concrete.
A subtler design point worth pausing on: the login flow uses a passkey (Touch ID / Face ID / hardware key) to authorize a scoped session key — time-limited, spending-capped, chain-bound — that the CLI then uses for signing. Your passkey never leaves the browser. The CLI holds a restricted credential, not your root key. This is the same pattern an agent should use: don't give it the keys to the kingdom; give it a scoped key that expires.
🔍 Find in repo. Open the wallet's
ARCHITECTURE.md(or skim the top-level Rust crates). In your own words: at what layer does the wallet hold the passkey-derived session key, and at what layer does it hand aPaymentProviderto the MPP middleware? The answer tells you the deployment shape for any agent you build on top of this.
Now answer the earlier predict
Why is WWW-Authenticate: Payment extensible rather than Tempo-specific? Because the moment you bake in one rail, you've forked the protocol from anyone who wants to use a different one. Stripe co-maintaining MPP is only possible because Core is rail-agnostic — Stripe contributes the methods/stripe/ directory, not edits to Core. Same shape Lightning, ACH, and any future rail will follow.
This is the same trait-split discipline you saw in artemis (Collector / Strategy / Executor) and in the validate-revm cross-check (provider-agnostic via the Alloy Provider trait). The pattern repeats in every serious protocol or framework: separate mechanics from policy from concrete implementation, and the future stays cheap.
Why this matters for what you build
Two angles, both real:
-
You ship a paid service. Wrap your endpoint with
Mpp::create(tempo(...))(or Stripe, or both). Agents and apps can pay per-request. No billing infrastructure, no API-key issuance, no rate-limit dashboards, no Stripe-portal integration. You charge what each request is worth and the protocol handles settlement. The aggregator from Lesson 7, the RPC endpoint from Lesson 3, the cheatcode harness from Lesson 6 — any of them could expose a paid surface this way. -
You ship a paid consumer. Bolt
PaymentMiddlewareonto a reqwest client. Now your agent — or your indexer, or your validator's observability stack — can pay any MPP-compatible endpoint without per-vendor integration. The MEV searcher from Lesson 1 wants paid mempool feeds? Plug in MPP. The wallet backend from Lesson 4 wants paid data oracles? Plug in MPP. The capstone router from Lesson 8 wants paid private order flow? Plug in MPP.
The genuine product idea worth chasing: a paid service that only an agent would want at this granularity — single-shot flight-status lookups, single-shot pricing oracles, single-shot LLM completions billed per token. Human-grade APIs don't price small enough; agent-grade APIs do because MPP makes per-request settlement cheap.
Recall checklist
Before moving on, confirm you can answer each of these without scrolling back:
- What HTTP status code does MPP claim, and what does the server's
WWW-Authenticateheader look like? - Name the three layers of the spec and what each one pins down.
- On the SDK server side, what does
Mpp::create(tempo(...))give you? On the client side, what doesPaymentMiddlewaredo? - Why is the Intents layer separate from Core? Give a concrete scenario where it matters.
- What's the trade-off between a one-shot
tempo requestand a session-modetempo request?
If you stumbled on 2 or 4, re-read the Core/Intents/Methods section before the next lesson.
Drill
- Read the IETF draft. Open
specs/core/draft-httpauth-payment-00.md. Skim until you can name three header fields thePaymentscheme defines and what each one carries. (45 min) - Stand up a paid endpoint. Clone
mpp-rs, look atexamples/, and run an axum server that charges 0.01 for one route. Hit it with curl, observe the 402. Hit it withtempo request, observe the 200. (2 hours) - Wrap an existing service. Pick one of your earlier-tier builds — say, the custom RPC endpoint from Lesson 3. Add
mpp's axum integration. Charge per call. Verify with the wallet. (3 hours) - Trace a session. With session-mode
tempo requestagainst a paid SSE endpoint, walk the network: when does the channel open? when do vouchers exchange? when does it settle? Usetempo wallet sessions listandcloseto inspect state. (1.5 hours) - Implement a custom provider. Pick a payment rail not in the SDK (your favorite Lesson 2's native asset). Implement
PaymentProvider(client) andChargeMethod(server). Test against your own paid endpoint. This is the test of whether the abstraction earns its keep. (4 hours)
Finish drill 3 and you have a paid endpoint deployable on real infrastructure. Finish drill 5 and you've internalized the protocol well enough to extend it.
Test gate
Per Test gate — every app in this tier ships with passing tests, this lesson's minimum gate covers the two failure modes that would either lock out paying users or let attackers double-spend:
- 402 on no payment, 200 on valid payment — the protocol's main contract. A request without a valid
X-PAYMENTheader returns HTTP 402 with the cost + receiving address. The same request with a valid micropayment returns 200 + the resource. - Replay protection — the same micropayment receipt cannot satisfy two requests. (Without this, an attacker captures one valid
X-PAYMENTheader and uses it to drain your endpoint indefinitely.) The second request with an already-used receipt returns 402 (or 409Conflict, depending on your design).
// tests/mpp_gate.rs
use reqwest::StatusCode;
#[tokio::test]
async fn returns_402_without_payment() {
let svc = test_endpoint().await;
let resp = reqwest::get(svc.url("/resource")).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYMENT_REQUIRED);
let body: PaymentRequired = resp.json().await.unwrap();
assert!(body.amount > U256::ZERO);
assert_eq!(body.recipient, svc.recipient_address());
}
#[tokio::test]
async fn returns_resource_with_valid_payment() {
let svc = test_endpoint().await;
let receipt = make_micropayment(&svc).await;
let resp = reqwest::Client::new()
.get(svc.url("/resource"))
.header("X-PAYMENT", receipt.encode())
.send().await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.text().await.unwrap(), EXPECTED_RESOURCE_BODY);
}
#[tokio::test]
async fn rejects_replayed_payment() {
let svc = test_endpoint().await;
let receipt = make_micropayment(&svc).await;
let header_value = receipt.encode();
// First use lands
let r1 = reqwest::Client::new()
.get(svc.url("/resource"))
.header("X-PAYMENT", &header_value)
.send().await.unwrap();
assert_eq!(r1.status(), StatusCode::OK);
// Second use must fail
let r2 = reqwest::Client::new()
.get(svc.url("/resource"))
.header("X-PAYMENT", &header_value)
.send().await.unwrap();
assert!(matches!(r2.status(), StatusCode::PAYMENT_REQUIRED | StatusCode::CONFLICT));
}
The lesson is not complete until all three pass against your endpoint running locally (with a forked Tempo testnet or anvil for the payment leg). A 402 endpoint that fails the replay test is a wallet-drainer waiting for someone to find your URL.
🧭 Where you are now in the stack: you've shipped the networking-layer payment protocol — HTTP 402 challenge, micropayment receipt, replay protection. Same abstraction pattern as TLS extending HTTP with crypto, applied to settlement. With this, the tier's 11 lessons are complete — every layer of the systems-engineering stack (networking, database, VM, authentication, concurrency) now has at least one application built end-to-end against it, with a test gate proving it works. That's the tier's promise delivered.
Expert continuation
You've built the application-layer endpoint. The Expert-tier counterpart is Payment-rail engineering — the L1 category when settlement is the product, which extracts the four category shifts (settlement assurance / fee abstraction / identity hooks / chain-agnostic surface) that explain why MPP exists as a protocol in the first place — and lets you read any payment-rail L1, not just Tempo's.
Summary (3 lines)
- HTTP 402 + Tempo MPP = sub-cent micropayments per API call. AI agents pay for API calls per request → agentic workflows become economical.
- Flow: server returns 402 + quote → client retries with signed payment → server verifies + logs receipt → batched on-chain settlement hourly.
- Adversarial: nonce + amount cap + merchant signature + rate limiting. Forecasts the next major surface area for the Rust EVM stack. Building track complete.