x402 turned HTTP 402 into a real payment flow. Clients hit your API, get payment requirements, sign an on-chain transaction, and retry. The facilitator settles it. It works.
But x402 is one protocol. It requires a wallet, an on-chain signature, and a facilitator round-trip. For some use cases — AI agents calling APIs in tight loops, metered SaaS backends, automated pipelines — that's the right tool. For others, a different payment rail makes more sense.
MPP (Machine Payment Protocol) is that other rail. Built on the Payment HTTP Authentication Scheme, MPP embeds the entire payment lifecycle in standard HTTP headers. The server issues aWWW-Authenticate: Payment challenge — HMAC-bound so its parameters can't be tampered with — and the client responds with an Authorization: Payment credential containing a method-specific proof.
The key difference: MPP is method-agnostic. The same protocol supports Tempo (on-chain stablecoins), Stripe (card payments), Lightning (Bitcoin invoices), or any custom method. The proof in the payload field changes — a Tempo credential contains a signed on-chain transaction, a Stripe credential contains a payment intent — but the HTTP flow is identical. x402 is tied to on-chain EVM/Solana settlement via a facilitator. MPP is a payment transport that works with any backend.
Neither replaces the other. That's why we built Dual-Channel.
Starting with @relai-fi/x402 v0.5.39, the SDK supports both. We call it Dual-Channel.
What Dual-Channel Means
One protect() middleware. Two accepted payment methods. The server doesn't choose — the client does.
When a request hits a protected endpoint with no payment:
- The server returns
402 Payment Requiredwith both x402 payment requirements in the body and aWWW-Authenticate: PaymentMPP challenge in the headers - An x402 client reads the body, signs an EIP-3009 transfer, retries with
X-Payment - An MPP client reads the
WWW-Authenticateheader, signs a Tempo credential, retries withAuthorization: Payment
req.payment is populated, a receipt header is set, next() is called. Your endpoint handler doesn't know or care which protocol paid for the request.
Server: Add MPP in One Line
import Relai from '@relai-fi/x402/server';
import { Mppx, tempo } from 'mppx/server';
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY,
methods: [
tempo.charge({
recipient: '0xYourWallet',
currency: '0x20C000000000000000000000b9537d11c60E8b50', // Tempo USDC
decimals: 6,
}),
],
});
const relai = new Relai({
network: 'base',
mpp: mppx, // enables Dual-Channel
});
app.get('/api/data', relai.protect({
payTo: '0xYourWallet',
price: 0.01,
}), (req, res) => {
res.json({ data: 'premium content', paidBy: req.x402Payer });
});
No changes to your route handlers. No conditional logic. The mpp option activates the second channel.
Client: Automatic Protocol Selection
import { createX402Client } from '@relai-fi/x402/client';
import { Mppx, tempo } from 'mppx/client';
import { privateKeyToAccount } from 'viem/accounts';
const client = createX402Client({
mpp: Mppx.create({
methods: [tempo.charge({
account: privateKeyToAccount(process.env.TEMPO_PRIVATE_KEY!),
})],
}),
});
// The client picks the right protocol automatically
const res = await client.fetch('https://api.example.com/data');
The client receives the 402, detects the WWW-Authenticate: Payment challenge, creates a credential, and retries. If the server doesn't support MPP, or if the MPP handshake fails, the client falls back to standard x402 — provided a wallet is configured.
You don't pick a protocol per-request. The SDK handles it.
Every Plugin Works on Both Channels
This was the non-negotiable. We didn't ship Dual-Channel until every plugin produced identical behavior regardless of which payment rail settled the request.
All four plugins from Stop Paying for Broken Endpoints — plus the free tier system — run in beforePaymentCheck, which executes before the payment method is even determined. They don't know if the next step is x402 or MPP. They don't need to.
Free Tier — First N Calls Free, Then Either Protocol Pays
import { freeTier } from '@relai-fi/x402/plugins';
const relai = new Relai({
network: 'base',
mpp: mppx,
plugins: [
freeTier({ perBuyerLimit: 10, resetPeriod: 'daily' }),
],
});
The free tier plugin counts calls per buyer. When the limit is reached, the next request gets a 402 with both channels available. The buyer's client — whether it has an EVM wallet or a Tempo account — picks the one it supports.
Request 1-10 -> 200 (free, X-Free-Calls-Remaining: 9...0)
Request 11 -> 402 + x402 body + WWW-Authenticate: Payment
-> Client auto-pays via MPP or x402
-> 200 + receipt
Shield — Block Payment When the Service Is Down
import { shield } from '@relai-fi/x402/plugins';
const relai = new Relai({
network: 'base',
mpp: mppx,
plugins: [
shield({ healthCheck: () => checkMyBackend(), cacheTtlMs: 10000 }),
],
});
Shield intercepts before the 402 is even generated. If the service is unhealthy, both x402 and MPP clients receive 503 — no challenge issued, no payment attempted.
Service healthy -> 402 -> payment (x402 or MPP) -> 200
Service down -> 503 (X-Shield-Status: unhealthy, Retry-After: 10)
Circuit Breaker — Tracks Failures Across Both Channels
import { circuitBreaker } from '@relai-fi/x402/plugins';
const relai = new Relai({
network: 'base',
mpp: mppx,
plugins: [
circuitBreaker({ failureThreshold: 5, resetTimeMs: 30000 }),
],
});
The circuit breaker uses afterSettled to track outcomes. This is where Dual-Channel parity required actual code changes. Previously, afterSettled was only called on success — for both protocols. Settlement failures were silent.
Now, afterSettled({ success: false }) fires when:
- x402: the facilitator
/settlereturnssuccess: false - MPP:
charge()returns a non-402 error status, or throws an exception
5 failures (x402 or MPP) -> Circuit OPEN -> 503
Wait 30s -> HALF-OPEN -> test requests allowed
2 successes -> CLOSED -> normal operation
Refund — Credits on Settlement Failure
import { refund } from '@relai-fi/x402/plugins';
const relai = new Relai({
network: 'base',
mpp: mppx,
plugins: [
refund({ mode: 'credit', onRefund: (e) => console.log(Credit: ${e.payer}) }),
],
});
When a settlement fails — facilitator timeout, MPP charge error — the refund plugin stores an in-memory credit keyed by buyer IP. The buyer's next request is free:
Request -> payment settles -> settlement fails
-> onRefund fires -> credit stored
Next request from same IP:
-> X-Refund-Credit: applied -> 200 (free)
> Note: This is a credit system, not an on-chain refund. The buyer gets a free pass on their next call. For on-chain refunds, use the onRefund callback to trigger your own settlement logic.
The Flow, Visualized
+-------------+
| Request |
+------+------+
|
+------v------+
| Plugins |
| freeTier |---> skip? ---> 200 (free)
| shield |---> down? --> 503
| circuitBkr |---> open? --> 503
| refund |---> credit? -> 200 (free)
+------+------+
| no skip, no reject
+------v------+
| 402 |
| + x402 body |
| + WWW-Auth |
+------+------+
|
+------------+------------+
| |
+------v------+ +------v------+
| X-Payment | |Authorization|
| (x402) | | Payment |
| | | (MPP) |
+------+------+ +------+------+
| |
+------v------+ +------v------+
| Facilitator | | mppx.charge |
| /settle | | (verify) |
+------+------+ +------+------+
| |
+------------+------------+
|
+------v------+
| afterSettled|
| (success |
| or failure)|
+------+------+
|
+------v------+
| 200 |
| + receipt |
+-------------+
x402 vs MPP — What's Different
| x402 | MPP | |
|---|---|---|
| Payment header | X-Payment (base64 JSON) | Authorization: Payment |
| Settlement | Facilitator /settle endpoint | Method-specific — Tempo settles on-chain, Stripe off-chain, Lightning via BOLT11 |
| Chain/Network | EVM (Base, Polygon, Avalanche...) or Solana | Any — the SDK ships with Tempo today, Stripe MPP coming soon |
| Client requirement | EVM or Solana wallet | Depends on method — Tempo account for now |
| Receipt | PAYMENT-RESPONSE header | Payment-Receipt header |
| Plugin support | Full | Full (identical) |
afterSettled on success | Called | Called |
afterSettled on failure | Called | Called (non-402 errors) |
Getting Started
npm install @relai-fi/x402 mppx viem
Full server with all plugins:
import Relai from '@relai-fi/x402/server';
import { freeTier, shield, circuitBreaker, refund } from '@relai-fi/x402/plugins';
import { Mppx, tempo } from 'mppx/server';
const mppx = Mppx.create({
secretKey: process.env.MPP_SECRET_KEY!,
methods: [
tempo.charge({
recipient: process.env.RECIPIENT_WALLET!,
currency: '0x20C000000000000000000000b9537d11c60E8b50',
decimals: 6,
}),
],
});
const relai = new Relai({
network: 'base',
mpp: mppx,
plugins: [
freeTier({ perBuyerLimit: 10, resetPeriod: 'daily' }),
shield({ healthUrl: 'https://my-api.com/health' }),
circuitBreaker({ failureThreshold: 5, resetTimeMs: 30000 }),
refund({ mode: 'credit' }),
],
});
Client:
import { createX402Client } from '@relai-fi/x402/client';
import { Mppx, tempo } from 'mppx/client';
import { privateKeyToAccount } from 'viem/accounts';
const client = createX402Client({
mpp: Mppx.create({
methods: [tempo.charge({
account: privateKeyToAccount(process.env.TEMPO_PRIVATE_KEY!),
})],
}),
});
const res = await client.fetch('https://api.example.com/premium');
What's Next
- Stripe MPP — same
mppinterface, backed by Stripe settlement instead of Tempo - Per-method routing — accept multiple MPP methods (Tempo + Stripe) on the same endpoint
- Dashboard metrics — MPP settlements alongside x402 in the RelAI dashboard
- Bridge + MPP — cross-chain payments with MPP as fallback when the buyer has no wallet on the target chain
Dual-Channel is available in @relai-fi/x402 v0.5.39+. See the examples on GitHub for full working code.
