Dual-Channel Payments: One SDK, Two Protocols - x402 and MPP Side by Side

RelAI Team
Mar 19, 2026 · 7 min read
Dual-Channel Payments: One SDK, Two Protocols - x402 and MPP Side by Side

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 a WWW-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:

  1. The server returns 402 Payment Required with both x402 payment requirements in the body and a WWW-Authenticate: Payment MPP challenge in the headers
  2. An x402 client reads the body, signs an EIP-3009 transfer, retries with X-Payment
  3. An MPP client reads the WWW-Authenticate header, signs a Tempo credential, retries with Authorization: Payment
Both paths end the same way: 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 /settle returns success: false
  • MPP: charge() returns a non-402 error status, or throws an exception
A 402 re-challenge — where the MPP handler asks the client to re-sign — is part of the normal MPP handshake. It is not counted as a failure.
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

x402MPP
Payment headerX-Payment (base64 JSON)Authorization: Payment
SettlementFacilitator /settle endpointMethod-specific — Tempo settles on-chain, Stripe off-chain, Lightning via BOLT11
Chain/NetworkEVM (Base, Polygon, Avalanche...) or SolanaAny — the SDK ships with Tempo today, Stripe MPP coming soon
Client requirementEVM or Solana walletDepends on method — Tempo account for now
ReceiptPAYMENT-RESPONSE headerPayment-Receipt header
Plugin supportFullFull (identical)
afterSettled on successCalledCalled
afterSettled on failureCalledCalled (non-402 errors)
The protocols serve different purposes. x402 is an on-chain payment protocol — it settles on EVM and Solana chains via a facilitator. MPP is a payment transport — it carries any payment method (Tempo, Stripe, Lightning) over HTTP headers using a standardized challenge/response. Dual-Channel lets your API accept both without choosing.

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 mpp interface, 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.

Understand x402 before you implement

This guide uses payment primitives from the x402 standard. Read the protocol overview for a complete flow, terminology, and integration FAQ.