mpp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMPP - Machine Payments Protocol
MPP - 机器支付协议
MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.
402 Payment RequiredThe core protocol spec is submitted to the IETF as the Payment HTTP Authentication Scheme.
MPP是由Tempo和Stripe联合撰写的开放协议,它将HTTP 标准化,用于机器对机器支付。客户端可在同一个HTTP请求中完成支付——无需账户、API密钥或结账流程。
402 Payment Required核心协议规范已作为Payment HTTP Authentication Scheme提交至IETF。
When to Use
适用场景
- Building a paid API that charges per request
- Adding a paywall to endpoints or content
- Enabling AI agents to pay for services autonomously
- MCP tool calls that require payment
- Pay-per-token streaming (LLM inference, content generation)
- Session-based metered billing (pay-as-you-go)
- Accepting stablecoins (Tempo), cards (Stripe), or Bitcoin (Lightning) for API access
- Building a payments proxy to gate existing APIs (OpenAI, Anthropic, etc.)
- 构建按请求收费的付费API
- 为端点或内容添加付费墙
- 支持AI Agent自主支付服务费用
- 需支付的MCP工具调用
- 按令牌付费的流服务(LLM推理、内容生成)
- 基于会话的计量计费(按使用付费)
- 接受稳定币(Tempo)、银行卡(Stripe)或比特币(Lightning)用于API访问
- 构建支付代理,为现有API(OpenAI、Anthropic等)添加付费限制
Core Architecture
核心架构
Three primitives power every MPP payment:
- Challenge - server-issued payment requirement (in header)
WWW-Authenticate: Payment - Credential - client-submitted payment proof (in header)
Authorization: Payment - Receipt - server confirmation of successful payment (in header)
Payment-Receipt
Client Server
│ (1) GET /resource │
├──────────────────────────────────────────────>│
│ (2) 402 + WWW-Authenticate: Payment │
│<──────────────────────────────────────────────┤
│ (3) Sign payment proof │
│ (4) GET /resource + Authorization: Payment │
├──────────────────────────────────────────────>│
│ (5) Verify + settle │
│ (6) 200 OK + Payment-Receipt │
│<──────────────────────────────────────────────┤三个核心原语支撑所有MPP支付:
- Challenge - 服务器下发的支付要求(位于请求头)
WWW-Authenticate: Payment - Credential - 客户端提交的支付凭证(位于请求头)
Authorization: Payment - Receipt - 服务器确认支付成功的凭证(位于请求头)
Payment-Receipt
Client Server
│ (1) GET /resource │
├──────────────────────────────────────────────>│
│ (2) 402 + WWW-Authenticate: Payment │
│<──────────────────────────────────────────────┤
│ (3) Sign payment proof │
│ (4) GET /resource + Authorization: Payment │
├──────────────────────────────────────────────>│
│ (5) Verify + settle │
│ (6) 200 OK + Payment-Receipt │
│<──────────────────────────────────────────────┤Payment Methods & Intents
支付方式与意图
MPP is payment-method agnostic. Each method defines its own settlement rail:
| Method | Rail | SDK Package | Status |
|---|---|---|---|
| Tempo | TIP-20 stablecoins on Tempo chain | | Production |
| Stripe | Cards, wallets via Shared Payment Tokens | | Production |
| Lightning | Bitcoin over Lightning Network | | Production |
| Card | Encrypted network tokens (Visa) | | Production |
| Custom | Any rail | | Extensible |
Two payment intents:
| Intent | Pattern | Best For |
|---|---|---|
| charge | One-time payment per request | API calls, content access, fixed-price endpoints |
| session | Pay-as-you-go over payment channels | LLM streaming, metered billing, high-frequency APIs |
MPP与支付方式无关,每种方式都定义了自己的结算渠道:
| 方式 | 结算渠道 | SDK包 | 状态 |
|---|---|---|---|
| Tempo | Tempo链上的TIP-20稳定币 | | 生产可用 |
| Stripe | 基于共享支付令牌的银行卡、钱包 | | 生产可用 |
| Lightning | Lightning网络上的比特币 | | 生产可用 |
| Card | 加密网络令牌(Visa) | | 生产可用 |
| 自定义 | 任意渠道 | | 可扩展 |
两种支付意图:
| 意图 | 模式 | 最佳适用场景 |
|---|---|---|
| charge | 每次请求一次性支付 | API调用、内容访问、固定价格端点 |
| session | 基于支付通道的按使用付费 | LLM流服务、计量计费、高频API |
Quick Start: Server (TypeScript)
快速开始:服务器端(TypeScript)
typescript
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000', // pathUSD
recipient: '0xYourAddress',
})],
})
export async function handler(request: Request) {
const result = await mppx.charge({ amount: '0.01' })(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
}Install:
npm install mppx viemtypescript
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({
methods: [tempo({
currency: '0x20c0000000000000000000000000000000000000', // pathUSD
recipient: '0xYourAddress',
})],
})
export async function handler(request: Request) {
const result = await mppx.charge({ amount: '0.01' })(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
}安装:
npm install mppx viemQuick Start: Client (TypeScript)
快速开始:客户端(TypeScript)
typescript
import { privateKeyToAccount } from 'viem/accounts'
import { Mppx, tempo } from 'mppx/client'
// Polyfills globalThis.fetch to handle 402 automatically
Mppx.create({
methods: [tempo({ account: privateKeyToAccount('0x...') })],
})
const res = await fetch('https://api.example.com/paid')
// Payment happens transparently when server returns 402typescript
import { privateKeyToAccount } from 'viem/accounts'
import { Mppx, tempo } from 'mppx/client'
// 为globalThis.fetch打补丁,自动处理402响应
Mppx.create({
methods: [tempo({ account: privateKeyToAccount('0x...') })],
})
const res = await fetch('https://api.example.com/paid')
// 当服务器返回402时,支付会自动完成Quick Start: Server (Python)
快速开始:服务器端(Python)
python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from mpp import Challenge
from mpp.server import Mpp
from mpp.methods.tempo import tempo, ChargeIntent
app = FastAPI()
mpp = Mpp.create(
method=tempo(
currency="0x20c0000000000000000000000000000000000000",
recipient="0xYourAddress",
intents={"charge": ChargeIntent()},
),
)
@app.get("/resource")
async def get_resource(request: Request):
result = await mpp.charge(
authorization=request.headers.get("Authorization"),
amount="0.50",
)
if isinstance(result, Challenge):
return JSONResponse(
status_code=402,
content={"error": "Payment required"},
headers={"WWW-Authenticate": result.to_www_authenticate(mpp.realm)},
)
credential, receipt = result
return {"data": "paid content"}Install:
pip install "pympp[tempo]"python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from mpp import Challenge
from mpp.server import Mpp
from mpp.methods.tempo import tempo, ChargeIntent
app = FastAPI()
mpp = Mpp.create(
method=tempo(
currency="0x20c0000000000000000000000000000000000000",
recipient="0xYourAddress",
intents={"charge": ChargeIntent()},
),
)
@app.get("/resource")
async def get_resource(request: Request):
result = await mpp.charge(
authorization=request.headers.get("Authorization"),
amount="0.50",
)
if isinstance(result, Challenge):
return JSONResponse(
status_code=402,
content={"error": "Payment required"},
headers={"WWW-Authenticate": result.to_www_authenticate(mpp.realm)},
)
credential, receipt = result
return {"data": "paid content"}安装:
pip install "pympp[tempo]"Quick Start: Server (Rust)
快速开始:服务器端(Rust)
rust
use mpp::server::{Mpp, tempo, TempoConfig};
use mpp::{parse_authorization, format_www_authenticate};
let mpp = Mpp::create(tempo(TempoConfig {
recipient: "0xYourAddress",
}))?;
let challenge = mpp.charge("0.50")?;
let header = format_www_authenticate(&challenge)?;
// Respond with 402 + WWW-Authenticate header
// On retry with credential:
let credential = parse_authorization(auth_header)?;
let receipt = mpp.verify_credential(&credential).await?;
// Respond with 200 + paid contentInstall:
cargo add mpp --features tempo,serverrust
use mpp::server::{Mpp, tempo, TempoConfig};
use mpp::{parse_authorization, format_www_authenticate};
let mpp = Mpp::create(tempo(TempoConfig {
recipient: "0xYourAddress",
}))?;
let challenge = mpp.charge("0.50")?;
let header = format_www_authenticate(&challenge)?;
// 返回402响应 + WWW-Authenticate请求头
// 当客户端携带凭证重试时:
let credential = parse_authorization(auth_header)?;
let receipt = mpp.verify_credential(&credential).await?;
// 返回200响应 + 付费内容安装:
cargo add mpp --features tempo,serverFramework Middleware (TypeScript)
框架中间件(TypeScript)
Each framework has its own import for ergonomic middleware:
typescript
// Next.js
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0...', recipient: '0x...' })] })
export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))
// Hono
import { Mppx, tempo } from 'mppx/hono'
app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))
// Express
import { Mppx, tempo } from 'mppx/express'
app.get('/resource', mppx.charge({ amount: '0.1' }), (req, res) => res.json({ data: '...' }))
// Elysia
import { Mppx, tempo } from 'mppx/elysia'
app.guard({ beforeHandle: mppx.charge({ amount: '0.1' }) }, (app) =>
app.get('/resource', () => ({ data: '...' }))
)每个框架都有对应的导入包,提供易用的中间件:
typescript
// Next.js
import { Mppx, tempo } from 'mppx/nextjs'
const mppx = Mppx.create({ methods: [tempo({ currency: '0x20c0...', recipient: '0x...' })] })
export const GET = mppx.charge({ amount: '0.1' })(() => Response.json({ data: '...' }))
// Hono
import { Mppx, tempo } from 'mppx/hono'
app.get('/resource', mppx.charge({ amount: '0.1' }), (c) => c.json({ data: '...' }))
// Express
import { Mppx, tempo } from 'mppx/express'
app.get('/resource', mppx.charge({ amount: '0.1' }), (req, res) => res.json({ data: '...' }))
// Elysia
import { Mppx, tempo } from 'mppx/elysia'
app.guard({ beforeHandle: mppx.charge({ amount: '0.1' }) }, (app) =>
app.get('/resource', () => ({ data: '...' }))
)Sessions: Pay-as-You-Go Streaming
会话:按使用付费的流服务
Sessions open a payment channel once, then use off-chain vouchers for each request - no blockchain transaction per request. Sub-100ms latency, near-zero per-request fees.
typescript
// Server - session endpoint
const result = await mppx.session({
amount: '0.001',
unitType: 'token',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
// Server - SSE streaming with per-word billing
const mppx = Mppx.create({
methods: [tempo({ currency: '0x20c0...', recipient: '0x...', sse: true })],
})
export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
async () => {
const words = ['hello', 'world']
return async function* (stream) {
for (const word of words) {
await stream.charge()
yield word
}
}
}
)
// Client - session with auto-managed channel
import { Mppx, tempo } from 'mppx/client'
Mppx.create({
methods: [tempo({ account, maxDeposit: '1' })], // Lock up to 1 pathUSD
})
const res = await fetch('http://localhost:3000/api/resource')
// 1st request: opens channel on-chain
// 2nd+ requests: off-chain vouchers (no on-chain tx)See for the full session lifecycle, escrow contracts, and SSE patterns.
references/sessions.md会话只需打开一次支付通道,之后每个请求使用链下凭证——无需每次请求都进行区块链交易。延迟低于100ms,每请求费用几乎为零。
typescript
// 服务器端 - 会话端点
const result = await mppx.session({
amount: '0.001',
unitType: 'token',
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
// 服务器端 - 按单词计费的SSE流服务
const mppx = Mppx.create({
methods: [tempo({ currency: '0x20c0...', recipient: '0x...', sse: true })],
})
export const GET = mppx.session({ amount: '0.001', unitType: 'word' })(
async () => {
const words = ['hello', 'world']
return async function* (stream) {
for (const word of words) {
await stream.charge()
yield word
}
}
}
)
// 客户端 - 自动管理通道的会话
import { Mppx, tempo } from 'mppx/client'
Mppx.create({
methods: [tempo({ account, maxDeposit: '1' })], // 最多锁定1 pathUSD
})
const res = await fetch('http://localhost:3000/api/resource')
// 第一次请求:在链上打开通道
// 第二次及之后的请求:使用链下凭证(无需链上交易)查看了解完整的会话生命周期、托管合约以及SSE模式。
references/sessions.mdMulti-Method Support
多支付方式支持
Accept Tempo stablecoins, Stripe cards, and Lightning Bitcoin on a single endpoint:
typescript
import Stripe from 'stripe'
import { Mppx, tempo, stripe } from 'mppx/server'
import { spark } from '@buildonspark/lightning-mpp-sdk/server'
const mppx = Mppx.create({
methods: [
tempo({ currency: '0x20c0...', recipient: '0x...' }),
stripe.charge({ client: new Stripe(key), networkId: 'internal', paymentMethodTypes: ['card'] }),
spark.charge({ mnemonic: process.env.MNEMONIC! }),
],
})
// 402 response advertises all methods; client picks one在单个端点同时接受Tempo稳定币、Stripe银行卡和Lightning比特币:
typescript
import Stripe from 'stripe'
import { Mppx, tempo, stripe } from 'mppx/server'
import { spark } from '@buildonspark/lightning-mpp-sdk/server'
const mppx = Mppx.create({
methods: [
tempo({ currency: '0x20c0...', recipient: '0x...' }),
stripe.charge({ client: new Stripe(key), networkId: 'internal', paymentMethodTypes: ['card'] }),
spark.charge({ mnemonic: process.env.MNEMONIC! }),
],
})
// 402响应会展示所有可用支付方式;客户端可选择其中一种Payments Proxy
支付代理
Gate existing APIs behind MPP payments:
typescript
import { openai, Proxy } from 'mppx/proxy'
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({ methods: [tempo()] })
const proxy = Proxy.create({
services: [
openai({
apiKey: 'sk-...', // pragma: allowlist secret
routes: {
'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }),
'GET /v1/models': mppx.free(), // mppx.free() marks a route as free (no payment)
},
}),
],
})为现有API添加MPP支付限制:
typescript
import { openai, Proxy } from 'mppx/proxy'
import { Mppx, tempo } from 'mppx/server'
const mppx = Mppx.create({ methods: [tempo()] })
const proxy = Proxy.create({
services: [
openai({
apiKey: 'sk-...', // pragma: allowlist secret
routes: {
'POST /v1/chat/completions': mppx.charge({ amount: '0.05' }),
'GET /v1/models': mppx.free(), // mppx.free()标记该路由为免费(无需支付)
},
}),
],
})MCP Transport
MCP传输
MCP tool calls can require payment using JSON-RPC error code :
-32042typescript
// Server - MCP with payment (import tempo from mppx/server, NOT mppx/tempo)
import { McpServer } from 'mppx/mcp-sdk/server'
import { tempo } from 'mppx/server'
const server = McpServer.wrap(baseServer, {
methods: [tempo.charge({ ... })],
secretKey: '...',
})
// Client - payment-aware MCP client (import tempo from mppx/client)
import { McpClient } from 'mppx/mcp-sdk/client'
import { tempo } from 'mppx/client'
const mcp = McpClient.wrap(client, { methods: [tempo({ account })] })
const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })See for the full MCP encoding (challenge in error.data.challenges, credential in _meta).
references/transports.mdMCP工具调用可通过JSON-RPC错误码要求支付:
-32042typescript
// 服务器端 - 带支付的MCP(从mppx/server导入tempo,而非mppx/tempo)
import { McpServer } from 'mppx/mcp-sdk/server'
import { tempo } from 'mppx/server'
const server = McpServer.wrap(baseServer, {
methods: [tempo.charge({ ... })],
secretKey: '...',
})
// 客户端 - 支持支付的MCP客户端(从mppx/client导入tempo)
import { McpClient } from 'mppx/mcp-sdk/client'
import { tempo } from 'mppx/client'
const mcp = McpClient.wrap(client, { methods: [tempo({ account })] })
const result = await mcp.callTool({ name: 'premium_tool', arguments: {} })查看了解完整的MCP编码(错误信息中的challenge位于error.data.challenges,凭证位于_meta)。
references/transports.mdTesting
测试
bash
undefinedbash
undefinedCreate an account (stored in keychain, auto-funded on testnet)
创建账户(存储在钥匙串中,在测试网自动充值)
npx mppx account create
npx mppx account create
Make a paid request
发起付费请求
npx mppx http://localhost:3000/resource
npx mppx http://localhost:3000/resource
Inspect challenge without paying
查看challenge而不进行支付
npx mppx --inspect http://localhost:3000/resource
undefinednpx mppx --inspect http://localhost:3000/resource
undefinedSDK Packages
SDK包
| Language | Package | Install |
|---|---|---|
| TypeScript | | |
| Python | | |
| Rust | | |
TypeScript subpath exports:
- Server: (generic),
mppx/server,mppx/hono,mppx/express,mppx/nextjs(framework middleware)mppx/elysia - Client:
mppx/client - Proxy:
mppx/proxy - MCP: ,
mppx/mcp-sdk/servermppx/mcp-sdk/client - SSE utilities: (exports
mppx/tempowithSessionfor SSE stream parsing)Session.Sse.iterateData
Always import and from the appropriate subpath for your context (e.g. for Hono, for generic/MCP server, for client). Note: and are NOT exported from - that subpath only exports .
Mppxtempomppx/honomppx/servermppx/clientMppxtempomppx/tempoSession| 语言 | 包名 | 安装命令 |
|---|---|---|
| TypeScript | | |
| Python | | |
| Rust | | |
TypeScript子路径导出:
- 服务器端:(通用)、
mppx/server、mppx/hono、mppx/express、mppx/nextjs(框架中间件)mppx/elysia - 客户端:
mppx/client - 代理:
mppx/proxy - MCP:、
mppx/mcp-sdk/servermppx/mcp-sdk/client - SSE工具:(导出
mppx/tempo,包含Session用于SSE流解析)Session.Sse.iterateData
请始终从适合您场景的子路径导入和(例如,Hono框架使用,通用/服务器端MCP使用,客户端使用)。注意:和不会从导出——该子路径仅导出。
Mppxtempomppx/honomppx/servermppx/clientMppxtempomppx/tempoSessionKey Concepts
核心概念
- Challenge/Credential/Receipt: The three protocol primitives. Challenge IDs are HMAC-SHA256 bound to prevent tampering. See
references/protocol-spec.md - Payment methods: Tempo (stablecoins), Stripe (cards), Lightning (Bitcoin), Card (network tokens), or custom. See method-specific references
- Intents: (one-time) and
charge(streaming). Seesessionfor session detailsreferences/sessions.md - Transports: HTTP (headers) and MCP (JSON-RPC). See
references/transports.md - Fee sponsorship: Server pays gas fees on behalf of clients (Tempo). See
references/tempo-method.md - Push/pull modes: Client broadcasts tx (push) or server broadcasts (pull). See
references/tempo-method.md - Custom methods: Implement any payment rail with . See
Method.from()references/custom-methods.md
- Challenge/Credential/Receipt:协议的三个核心原语。Challenge ID通过HMAC-SHA256绑定,防止篡改。查看
references/protocol-spec.md - 支付方式:Tempo(稳定币)、Stripe(银行卡)、Lightning(比特币)、Card(网络令牌)或自定义方式。查看各方式的参考文档
- 意图:(一次性支付)和
charge(流服务)。查看session了解会话详情references/sessions.md - 传输方式:HTTP(请求头)和MCP(JSON-RPC)。查看
references/transports.md - 费用赞助:服务器代表客户端支付gas费用(Tempo)。查看
references/tempo-method.md - 推送/拉取模式:客户端广播交易(推送)或服务器广播(拉取)。查看
references/tempo-method.md - 自定义支付方式:使用实现任意支付渠道。查看
Method.from()references/custom-methods.md
Production Gotchas
生产环境注意事项
Setup
配置
Self-payment trap: The payer and recipient cannot be the same wallet address. When testing with , create a separate client account () and fund it separately.
npx mppxnpx mppx account create -a clientRecipient wallet initialization: TIP-20 token accounts on Tempo must be initialized before they can receive tokens (similar to Solana ATAs). Send a tiny amount (e.g. 0.01 USDC) to the recipient address first: .
tempo wallet transfer 0.01 0x20C000000000000000000000b9537d11c60E8b50 <recipient>自支付陷阱:付款方和收款方不能是同一个钱包地址。使用测试时,请创建单独的客户端账户()并单独充值。
npx mppxnpx mppx account create -a client收款钱包初始化:Tempo链上的TIP-20代币账户在接收代币前必须先初始化(类似Solana的ATA)。先向收款地址发送少量代币(例如0.01 USDC):。
tempo wallet transfer 0.01 0x20C000000000000000000000b9537d11c60E8b50 <recipient>Server
服务器端
tempo()tempo({ ... })chargesessionstoresse: { poll: true }typescript
import { Mppx, Store, tempo } from 'mppx/server'
Mppx.create({
methods: [
tempo.charge({ currency, recipient }),
tempo.session({ currency, recipient, store: Store.memory(), sse: { poll: true } }),
],
secretKey,
})Hono multiple headers: replaces by default. When emitting multiple values (e.g. charge + session intents), the second call silently overwrites the first. Use :
c.header(name, value)WWW-Authenticate{ append: true }typescript
c.header('WWW-Authenticate', chargeWwwAuth)
c.header('WWW-Authenticate', sessionWwwAuth, { append: true })CORS headers: and must be listed in or browsers/clients won't see them.
WWW-AuthenticatePayment-Receiptaccess-control-expose-headersSSE utilities import path: is exported from , NOT :
Session.Sse.iterateDatamppx/tempomppx/servertypescript
import { Mppx, Store, tempo } from 'mppx/server'
import { Session } from 'mppx/tempo'
const iterateSseData = Session.Sse.iterateDatatempo()tempo({ ... })chargesessionstoresse: { poll: true }typescript
import { Mppx, Store, tempo } from 'mppx/server'
Mppx.create({
methods: [
tempo.charge({ currency, recipient }),
tempo.session({ currency, recipient, store: Store.memory(), sse: { poll: true } }),
],
secretKey,
})Hono多请求头问题:默认会覆盖之前的请求头。当返回多个值时(例如charge和session意图),第二次调用会静默覆盖第一次。请使用:
c.header(name, value)WWW-Authenticate{ append: true }typescript
c.header('WWW-Authenticate', chargeWwwAuth)
c.header('WWW-Authenticate', sessionWwwAuth, { append: true })CORS请求头:和必须添加到中,否则浏览器或客户端无法获取到它们。
WWW-AuthenticatePayment-Receiptaccess-control-expose-headersSSE工具导入路径:从导出,而非:
Session.Sse.iterateDatamppx/tempomppx/servertypescript
import { Mppx, Store, tempo } from 'mppx/server'
import { Session } from 'mppx/tempo'
const iterateSseData = Session.Sse.iterateDataStores
存储
BigInt serialization: mppx stores channel state with BigInt values (from the library). throws . Use (handles it via ox's ) or . Plain Redis/ioredis with -based adapters will corrupt state.
oxJSON.stringify"Do not know how to serialize a BigInt"Store.memory()Json.parse/stringifyStore.upstash()JSON.stringifyNo built-in TTL: Custom store implementations must add explicit TTL/expiry on entries, otherwise channel state grows unboundedly. mppx's built-in stores handle this automatically.
Polling mode: If your store doesn't implement the optional method (e.g. custom Redis/ioredis adapters), pass to . Otherwise SSE streams will hang waiting for event-driven wakeups that never come.
waitForUpdate()sse: { poll: true }tempo.session()BigInt序列化:mppx使用BigInt值存储通道状态(来自库)。会抛出错误。请使用(通过ox的处理)或。使用的普通Redis/ioredis适配器会损坏状态。
oxJSON.stringify"Do not know how to serialize a BigInt"Store.memory()Json.parse/stringifyStore.upstash()JSON.stringify无内置TTL:自定义存储实现必须为条目添加显式的TTL/过期时间,否则通道状态会无限增长。mppx的内置存储会自动处理此问题。
轮询模式:如果您的存储未实现可选的方法(例如自定义Redis/ioredis适配器),请在中传入。否则SSE流会一直等待事件驱动的唤醒信号,导致挂起。
waitForUpdate()tempo.session()sse: { poll: true }Credential-Based Routing (Not Body-Based)
基于凭证的路由(而非基于请求体)
Session voucher POSTs have no body. Mid-stream voucher POSTs carry only - no JSON body. If your middleware decides charge vs session based on , vouchers will hit the charge path and fail with "credential amount does not match this route's requirements." Check the credential's intent instead.
Authorization: Paymentbody.streamClone the request before reading the body. consumes the Request body. If you parse the body first and then pass the original request to or , the mppx handler gets an empty body and returns 402. Clone before reading:
request.json()mppx.session()mppx.charge()typescript
import { Credential } from 'mppx'
let isSessionCredential = false
try {
const credential = Credential.fromRequest(c.req.raw)
isSessionCredential = credential.challenge.intent === 'session'
} catch {
// No credential - continue to normal flow
}
// Clone BEFORE reading body - mppx handlers need to read it too
const raw = c.req.raw.clone()
const body = await c.req.json().catch(() => ({}))
if (isSessionCredential && !wantStream) {
// Session voucher - route to mppx.session() directly.
// The session handler recognizes the voucher, updates channel balance,
// and returns 200 without needing the route handler.
const result = await mppx.session({ amount: tickCost, unitType: 'token' })(raw)
if (result.status === 402) return result.challenge
return result.withReceipt(new Response(null, { status: 200 }))
}
// All other mppx calls must also use `raw`, not `c.req.raw`会话凭证POST请求无请求体:流服务中的凭证POST请求仅携带——没有JSON请求体。如果您的中间件根据判断是charge还是session,凭证请求会进入charge路径并失败,提示"credential amount does not match this route's requirements"。请检查凭证的意图来判断。
Authorization: Paymentbody.stream读取请求体前先克隆请求:会消耗Request的请求体。如果您先解析请求体,再将原始请求传递给或,mppx处理器会得到空请求体并返回402。请在读取前克隆请求:
request.json()mppx.session()mppx.charge()typescript
import { Credential } from 'mppx'
let isSessionCredential = false
try {
const credential = Credential.fromRequest(c.req.raw)
isSessionCredential = credential.challenge.intent === 'session'
} catch {
// 无凭证 - 继续正常流程
}
// 读取请求体前先克隆 - mppx处理器也需要读取请求体
const raw = c.req.raw.clone()
const body = await c.req.json().catch(() => ({}))
if (isSessionCredential && !wantStream) {
// 会话凭证 - 直接路由到mppx.session()
// 会话处理器会识别凭证,更新通道余额,
// 并返回200响应,无需调用路由处理器。
const result = await mppx.session({ amount: tickCost, unitType: 'token' })(raw)
if (result.status === 402) return result.challenge
return result.withReceipt(new Response(null, { status: 200 }))
}
// 所有其他mppx调用也必须使用`raw`,而非`c.req.raw`Pricing & Streaming
定价与流服务
Cheap model zero-charge floor: Tempo USDC has 6-decimal precision. For very cheap models, per-token cost like rounds to via - effectively zero. Add a minimum tick cost floor:
(0.10 / 1_000_000) * 1.3 = 0.00000013"0.000000"toFixed(6)typescript
const MIN_TICK_COST = 0.000001 // smallest Tempo USDC unit (6 decimals)
const tickCost = Math.max((outputRate / 1_000_000) * margin, MIN_TICK_COST)SSE chunks != tokens: OpenRouter/LLM SSE chunks don't map 1:1 to tokens (one chunk may contain 1-3 tokens). Per-SSE-event is an acceptable approximation, consistent with the mppx examples.
stream.charge()Sequential input tick latency: is serial per call (Redis GET + SET per charge, serialized by per-channelId mutex). Charging N input ticks upfront adds N round-trips of latency to time-to-first-token. No bulk API exists yet.
stream.charge()stream.chargeMultiple(n)Add upstream timeouts: Always use on upstream fetches (e.g. to OpenRouter). A stalled upstream holds the payment channel open with no progress and no timeout, locking client funds.
AbortSignal.timeout()低价模型零收费下限:Tempo USDC有6位小数精度。对于非常便宜的模型,每令牌成本如会通过四舍五入为——实际为零收费。请添加最低计费单位:
(0.10 / 1_000_000) * 1.3 = 0.00000013toFixed(6)"0.000000"typescript
const MIN_TICK_COST = 0.000001 // 最小的Tempo USDC单位(6位小数)
const tickCost = Math.max((outputRate / 1_000_000) * margin, MIN_TICK_COST)SSE块 != 令牌:OpenRouter/LLM的SSE块与令牌并非1:1对应(一个块可能包含1-3个令牌)。每个SSE事件调用是可接受的近似值,与mppx示例一致。
stream.charge()顺序输入计费延迟:每次调用是串行的(每个计费单位对应一次Redis GET + SET,通过channelId互斥锁串行处理)。预先收取N个输入令牌的费用会增加N次往返延迟,影响首次令牌生成时间。目前尚无批量 API。
stream.charge()stream.chargeMultiple(n)添加上游超时:请始终为上游请求(例如调用OpenRouter)使用。如果上游服务停滞,会导致支付通道一直处于打开状态且无进展、无超时,锁定客户端资金。
AbortSignal.timeout()Infrastructure
基础设施
Nginx proxy buffer overflow: Large 402 response headers (especially when combining x402 + MPP, or multiple charge/session intents) can exceed nginx's default 4k , causing 502 Bad Gateway. The header alone can be ~3KB+ base64. Fix with nginx annotation:
proxy_buffer_sizePAYMENT-REQUIREDyaml
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"Debug tip: If you get 502 but pod logs show no incoming requests, port-forward directly to the pod () and curl localhost - if you get a proper 402, the issue is in the ingress/proxy layer, not the app.
kubectl port-forward <pod> 9999:8080Nginx代理缓冲区溢出:较大的402响应头(尤其是同时使用x402 + MPP,或多个charge/session意图时)可能超过Nginx默认的4k ,导致502 Bad Gateway。仅请求头的base64编码就可能超过3KB。可通过Nginx注解修复:
proxy_buffer_sizePAYMENT-REQUIREDyaml
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"调试技巧:如果您收到502但Pod日志中无请求记录,请直接端口转发到Pod()并curl localhost——如果返回正常的402,问题出在入口/代理层,而非应用程序。
kubectl port-forward <pod> 9999:8080Client / Tempo CLI
客户端 / Tempo CLI
Stale sessions after redeploy: When the server redeploys and loses in-memory session state, clients get . Fix by closing stale sessions: or . Sessions have a dispute window (4-15 min) before auto-clearing.
"Session invalidation claim for channel 0x... was not confirmed on-chain"tempo wallet sessions closetempo wallet sessions syncTempo CLI SSE bug: The Tempo CLI (as of v1.4.3) fails with (exit code 3) on SSE responses. Server-side streaming works correctly - the bug is purely client-side SSE parsing. Verify success via server logs instead of CLI exit code.
E_NETWORK: error decoding response body重新部署后会话失效:当服务器重新部署并丢失内存中的会话状态时,客户端会收到错误。可通过关闭失效会话修复:或。会话在自动清除前有4-15分钟的争议窗口。
"Session invalidation claim for channel 0x... was not confirmed on-chain"tempo wallet sessions closetempo wallet sessions syncTempo CLI SSE bug:Tempo CLI(v1.4.3版本)在处理SSE响应时会失败,提示(退出码3)。服务器端流服务工作正常——该bug仅存在于客户端SSE解析。请通过服务器日志验证成功,而非CLI退出码。
E_NETWORK: error decoding response bodyReferences
参考文档
| File | Content |
|---|---|
| Core protocol: Challenge/Credential/Receipt structure, status codes, error handling, security, caching, extensibility |
| mppx TypeScript SDK: server/client/middleware patterns, proxy, MCP SDK, CLI, Mppx.create options |
| Tempo payment method: charge + session intents, fee sponsorship, push/pull modes, auto-swap, testnet/mainnet config |
| Stripe payment method: SPT flow, server/client config, Stripe Elements, createToken proxy, metadata |
| Session intent deep-dive: payment channels, voucher signing, SSE streaming, escrow contracts, top-up, close |
| HTTP and MCP transport bindings: header encoding, JSON-RPC error codes, comparison |
| pympp Python SDK: FastAPI/server patterns, async client, streaming sessions |
| mpp Rust SDK: server/client, feature flags, reqwest middleware |
| Lightning payment method: charge (BOLT11), session (bearer tokens), Spark SDK |
| Custom payment methods: Method.from, Method.toClient, Method.toServer patterns |
| 文件 | 内容 |
|---|---|
| 核心协议:Challenge/Credential/Receipt结构、状态码、错误处理、安全性、缓存、可扩展性 |
| mppx TypeScript SDK:服务器端/客户端/中间件模式、代理、MCP SDK、CLI、Mppx.create选项 |
| Tempo支付方式:charge + session意图、费用赞助、推送/拉取模式、自动交换、测试网/主网配置 |
| Stripe支付方式:SPT流程、服务器端/客户端配置、Stripe Elements、createToken代理、元数据 |
| 会话意图深度解析:支付通道、凭证签名、SSE流服务、托管合约、充值、关闭 |
| HTTP和MCP传输绑定:请求头编码、JSON-RPC错误码、对比 |
| pympp Python SDK:FastAPI/服务器端模式、异步客户端、流会话 |
| mpp Rust SDK:服务器端/客户端、特性标志、reqwest中间件 |
| Lightning支付方式:charge(BOLT11)、session(Bearer令牌)、Spark SDK |
| 自定义支付方式:Method.from、Method.toClient、Method.toServer模式 |
Official Resources
官方资源
- Website: https://mpp.dev
- GitHub: https://github.com/wevm/mppx (TypeScript SDK)
- Protocol spec: https://paymentauth.org
- Stripe docs: https://docs.stripe.com/payments/machine/mpp
- Tempo docs: https://docs.tempo.xyz
- LLM docs: https://mpp.dev/llms-full.txt
- 官网:https://mpp.dev
- GitHub:https://github.com/wevm/mppx(TypeScript SDK)
- 协议规范:https://paymentauth.org
- Stripe文档:https://docs.stripe.com/payments/machine/mpp
- Tempo文档:https://docs.tempo.xyz
- LLM文档:https://mpp.dev/llms-full.txt