mpp

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MPP - Machine Payments Protocol

MPP - 机器支付协议

MPP is an open protocol (co-authored by Tempo and Stripe) that standardizes HTTP
402 Payment Required
for machine-to-machine payments. Clients pay in the same HTTP request - no accounts, API keys, or checkout flows needed.
The core protocol spec is submitted to the IETF as the Payment HTTP Authentication Scheme.
MPP是由Tempo和Stripe联合撰写的开放协议,它将HTTP
402 Payment Required
标准化,用于机器对机器支付。客户端可在同一个HTTP请求中完成支付——无需账户、API密钥或结账流程。
核心协议规范已作为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:
  1. Challenge - server-issued payment requirement (in
    WWW-Authenticate: Payment
    header)
  2. Credential - client-submitted payment proof (in
    Authorization: Payment
    header)
  3. Receipt - server confirmation of successful payment (in
    Payment-Receipt
    header)
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支付:
  1. Challenge - 服务器下发的支付要求(位于
    WWW-Authenticate: Payment
    请求头)
  2. Credential - 客户端提交的支付凭证(位于
    Authorization: Payment
    请求头)
  3. 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:
MethodRailSDK PackageStatus
TempoTIP-20 stablecoins on Tempo chain
mppx
(built-in)
Production
StripeCards, wallets via Shared Payment Tokens
mppx
(built-in)
Production
LightningBitcoin over Lightning Network
@buildonspark/lightning-mpp-sdk
Production
CardEncrypted network tokens (Visa)
mpp-card
Production
CustomAny rail
Method.from()
+
Method.toClient/toServer
Extensible
Two payment intents:
IntentPatternBest For
chargeOne-time payment per requestAPI calls, content access, fixed-price endpoints
sessionPay-as-you-go over payment channelsLLM streaming, metered billing, high-frequency APIs
MPP与支付方式无关,每种方式都定义了自己的结算渠道:
方式结算渠道SDK包状态
TempoTempo链上的TIP-20稳定币
mppx
(内置)
生产可用
Stripe基于共享支付令牌的银行卡、钱包
mppx
(内置)
生产可用
LightningLightning网络上的比特币
@buildonspark/lightning-mpp-sdk
生产可用
Card加密网络令牌(Visa)
mpp-card
生产可用
自定义任意渠道
Method.from()
+
Method.toClient/toServer
可扩展
两种支付意图:
意图模式最佳适用场景
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 viem
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: '...' }))
}
安装:
npm install mppx viem

Quick 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 402
typescript
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 content
Install:
cargo add mpp --features tempo,server
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)?;
// 返回402响应 + WWW-Authenticate请求头

// 当客户端携带凭证重试时:
let credential = parse_authorization(auth_header)?;
let receipt = mpp.verify_credential(&credential).await?;
// 返回200响应 + 付费内容
安装:
cargo add mpp --features tempo,server

Framework 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
references/sessions.md
for the full session lifecycle, escrow contracts, and SSE patterns.
会话只需打开一次支付通道,之后每个请求使用链下凭证——无需每次请求都进行区块链交易。延迟低于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')
// 第一次请求:在链上打开通道
// 第二次及之后的请求:使用链下凭证(无需链上交易)
查看
references/sessions.md
了解完整的会话生命周期、托管合约以及SSE模式。

Multi-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
-32042
:
typescript
// 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
references/transports.md
for the full MCP encoding (challenge in error.data.challenges, credential in _meta).
MCP工具调用可通过JSON-RPC错误码
-32042
要求支付:
typescript
// 服务器端 - 带支付的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: {} })
查看
references/transports.md
了解完整的MCP编码(错误信息中的challenge位于error.data.challenges,凭证位于_meta)。

Testing

测试

bash
undefined
bash
undefined

Create an account (stored in keychain, auto-funded on testnet)

创建账户(存储在钥匙串中,在测试网自动充值)

npx mppx account create
npx mppx account create

Make a paid request

发起付费请求

Inspect challenge without paying

查看challenge而不进行支付

undefined
undefined

SDK Packages

SDK包

LanguagePackageInstall
TypeScript
mppx
npm install mppx
Python
pympp
pip install pympp
or
pip install "pympp[tempo]"
Rust
mpp
cargo add mpp --features tempo,client,server
TypeScript subpath exports:
  • Server:
    mppx/server
    (generic),
    mppx/hono
    ,
    mppx/express
    ,
    mppx/nextjs
    ,
    mppx/elysia
    (framework middleware)
  • Client:
    mppx/client
  • Proxy:
    mppx/proxy
  • MCP:
    mppx/mcp-sdk/server
    ,
    mppx/mcp-sdk/client
  • SSE utilities:
    mppx/tempo
    (exports
    Session
    with
    Session.Sse.iterateData
    for SSE stream parsing)
Always import
Mppx
and
tempo
from the appropriate subpath for your context (e.g.
mppx/hono
for Hono,
mppx/server
for generic/MCP server,
mppx/client
for client). Note:
Mppx
and
tempo
are NOT exported from
mppx/tempo
- that subpath only exports
Session
.
语言包名安装命令
TypeScript
mppx
npm install mppx
Python
pympp
pip install pympp
pip install "pympp[tempo]"
Rust
mpp
cargo add mpp --features tempo,client,server
TypeScript子路径导出:
  • 服务器端:
    mppx/server
    (通用)、
    mppx/hono
    mppx/express
    mppx/nextjs
    mppx/elysia
    (框架中间件)
  • 客户端:
    mppx/client
  • 代理:
    mppx/proxy
  • MCP:
    mppx/mcp-sdk/server
    mppx/mcp-sdk/client
  • SSE工具:
    mppx/tempo
    (导出
    Session
    ,包含
    Session.Sse.iterateData
    用于SSE流解析)
请始终从适合您场景的子路径导入
Mppx
tempo
(例如,Hono框架使用
mppx/hono
,通用/服务器端MCP使用
mppx/server
,客户端使用
mppx/client
)。注意:
Mppx
tempo
不会从
mppx/tempo
导出——该子路径仅导出
Session

Key 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:
    charge
    (one-time) and
    session
    (streaming). See
    references/sessions.md
    for session details
  • 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
    Method.from()
    . See
    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
npx mppx
, create a separate client account (
npx mppx account create -a client
) and fund it separately.
Recipient 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 mppx
测试时,请创建单独的客户端账户(
npx mppx account create -a client
)并单独充值。
收款钱包初始化:Tempo链上的TIP-20代币账户在接收代币前必须先初始化(类似Solana的ATA)。先向收款地址发送少量代币(例如0.01 USDC):
tempo wallet transfer 0.01 0x20C000000000000000000000b9537d11c60E8b50 <recipient>

Server

服务器端

tempo()
vs explicit registration
:
tempo({ ... })
registers both
charge
and
session
intents with shared config. When you need different config per intent (e.g. session needs
store
and
sse: { poll: true }
but charge doesn't), register them explicitly:
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:
c.header(name, value)
replaces by default. When emitting multiple
WWW-Authenticate
values (e.g. charge + session intents), the second call silently overwrites the first. Use
{ append: true }
:
typescript
c.header('WWW-Authenticate', chargeWwwAuth)
c.header('WWW-Authenticate', sessionWwwAuth, { append: true })
CORS headers:
WWW-Authenticate
and
Payment-Receipt
must be listed in
access-control-expose-headers
or browsers/clients won't see them.
SSE utilities import path:
Session.Sse.iterateData
is exported from
mppx/tempo
, NOT
mppx/server
:
typescript
import { Mppx, Store, tempo } from 'mppx/server'
import { Session } from 'mppx/tempo'
const iterateSseData = Session.Sse.iterateData
tempo()
vs 显式注册
tempo({ ... })
会使用共享配置同时注册
charge
session
意图。当需要为不同意图配置不同参数时(例如,会话需要
store
sse: { 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多请求头问题
c.header(name, value)
默认会覆盖之前的请求头。当返回多个
WWW-Authenticate
值时(例如charge和session意图),第二次调用会静默覆盖第一次。请使用
{ append: true }
typescript
c.header('WWW-Authenticate', chargeWwwAuth)
c.header('WWW-Authenticate', sessionWwwAuth, { append: true })
CORS请求头
WWW-Authenticate
Payment-Receipt
必须添加到
access-control-expose-headers
中,否则浏览器或客户端无法获取到它们。
SSE工具导入路径
Session.Sse.iterateData
mppx/tempo
导出,而非
mppx/server
typescript
import { Mppx, Store, tempo } from 'mppx/server'
import { Session } from 'mppx/tempo'
const iterateSseData = Session.Sse.iterateData

Stores

存储

BigInt serialization: mppx stores channel state with BigInt values (from the
ox
library).
JSON.stringify
throws
"Do not know how to serialize a BigInt"
. Use
Store.memory()
(handles it via ox's
Json.parse/stringify
) or
Store.upstash()
. Plain Redis/ioredis with
JSON.stringify
-based adapters will corrupt state.
No 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
waitForUpdate()
method (e.g. custom Redis/ioredis adapters), pass
sse: { poll: true }
to
tempo.session()
. Otherwise SSE streams will hang waiting for event-driven wakeups that never come.
BigInt序列化:mppx使用BigInt值存储通道状态(来自
ox
库)。
JSON.stringify
会抛出
"Do not know how to serialize a BigInt"
错误。请使用
Store.memory()
(通过ox的
Json.parse/stringify
处理)或
Store.upstash()
。使用
JSON.stringify
的普通Redis/ioredis适配器会损坏状态。
无内置TTL:自定义存储实现必须为条目添加显式的TTL/过期时间,否则通道状态会无限增长。mppx的内置存储会自动处理此问题。
轮询模式:如果您的存储未实现可选的
waitForUpdate()
方法(例如自定义Redis/ioredis适配器),请在
tempo.session()
中传入
sse: { poll: true }
。否则SSE流会一直等待事件驱动的唤醒信号,导致挂起。

Credential-Based Routing (Not Body-Based)

基于凭证的路由(而非基于请求体)

Session voucher POSTs have no body. Mid-stream voucher POSTs carry only
Authorization: Payment
- no JSON body. If your middleware decides charge vs session based on
body.stream
, vouchers will hit the charge path and fail with "credential amount does not match this route's requirements." Check the credential's intent instead.
Clone the request before reading the body.
request.json()
consumes the Request body. If you parse the body first and then pass the original request to
mppx.session()
or
mppx.charge()
, the mppx handler gets an empty body and returns 402. Clone before reading:
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请求仅携带
Authorization: Payment
——没有JSON请求体。如果您的中间件根据
body.stream
判断是charge还是session,凭证请求会进入charge路径并失败,提示"credential amount does not match this route's requirements"。请检查凭证的意图来判断。
读取请求体前先克隆请求
request.json()
会消耗Request的请求体。如果您先解析请求体,再将原始请求传递给
mppx.session()
mppx.charge()
,mppx处理器会得到空请求体并返回402。请在读取前克隆请求:
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
(0.10 / 1_000_000) * 1.3 = 0.00000013
rounds to
"0.000000"
via
toFixed(6)
- effectively zero. Add a minimum tick cost floor:
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
stream.charge()
is an acceptable approximation, consistent with the mppx examples.
Sequential input tick latency:
stream.charge()
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
stream.chargeMultiple(n)
API exists yet.
Add upstream timeouts: Always use
AbortSignal.timeout()
on upstream fetches (e.g. to OpenRouter). A stalled upstream holds the payment channel open with no progress and no timeout, locking client funds.
低价模型零收费下限:Tempo USDC有6位小数精度。对于非常便宜的模型,每令牌成本如
(0.10 / 1_000_000) * 1.3 = 0.00000013
会通过
toFixed(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事件调用
stream.charge()
是可接受的近似值,与mppx示例一致。
顺序输入计费延迟
stream.charge()
每次调用是串行的(每个计费单位对应一次Redis GET + SET,通过channelId互斥锁串行处理)。预先收取N个输入令牌的费用会增加N次往返延迟,影响首次令牌生成时间。目前尚无批量
stream.chargeMultiple(n)
API。
添加上游超时:请始终为上游请求(例如调用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
proxy_buffer_size
, causing 502 Bad Gateway. The
PAYMENT-REQUIRED
header alone can be ~3KB+ base64. Fix with nginx annotation:
yaml
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 (
kubectl port-forward <pod> 9999:8080
) and curl localhost - if you get a proper 402, the issue is in the ingress/proxy layer, not the app.
Nginx代理缓冲区溢出:较大的402响应头(尤其是同时使用x402 + MPP,或多个charge/session意图时)可能超过Nginx默认的4k
proxy_buffer_size
,导致502 Bad Gateway。仅
PAYMENT-REQUIRED
请求头的base64编码就可能超过3KB。可通过Nginx注解修复:
yaml
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
调试技巧:如果您收到502但Pod日志中无请求记录,请直接端口转发到Pod(
kubectl port-forward <pod> 9999:8080
)并curl localhost——如果返回正常的402,问题出在入口/代理层,而非应用程序。

Client / Tempo CLI

客户端 / Tempo CLI

Stale sessions after redeploy: When the server redeploys and loses in-memory session state, clients get
"Session invalidation claim for channel 0x... was not confirmed on-chain"
. Fix by closing stale sessions:
tempo wallet sessions close
or
tempo wallet sessions sync
. Sessions have a dispute window (4-15 min) before auto-clearing.
Tempo CLI SSE bug: The Tempo CLI (as of v1.4.3) fails with
E_NETWORK: error decoding response body
(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.
重新部署后会话失效:当服务器重新部署并丢失内存中的会话状态时,客户端会收到
"Session invalidation claim for channel 0x... was not confirmed on-chain"
错误。可通过关闭失效会话修复:
tempo wallet sessions close
tempo wallet sessions sync
。会话在自动清除前有4-15分钟的争议窗口。
Tempo CLI SSE bug:Tempo CLI(v1.4.3版本)在处理SSE响应时会失败,提示
E_NETWORK: error decoding response body
(退出码3)。服务器端流服务工作正常——该bug仅存在于客户端SSE解析。请通过服务器日志验证成功,而非CLI退出码。

References

参考文档

FileContent
references/protocol-spec.md
Core protocol: Challenge/Credential/Receipt structure, status codes, error handling, security, caching, extensibility
references/typescript-sdk.md
mppx TypeScript SDK: server/client/middleware patterns, proxy, MCP SDK, CLI, Mppx.create options
references/tempo-method.md
Tempo payment method: charge + session intents, fee sponsorship, push/pull modes, auto-swap, testnet/mainnet config
references/stripe-method.md
Stripe payment method: SPT flow, server/client config, Stripe Elements, createToken proxy, metadata
references/sessions.md
Session intent deep-dive: payment channels, voucher signing, SSE streaming, escrow contracts, top-up, close
references/transports.md
HTTP and MCP transport bindings: header encoding, JSON-RPC error codes, comparison
references/python-sdk.md
pympp Python SDK: FastAPI/server patterns, async client, streaming sessions
references/rust-sdk.md
mpp Rust SDK: server/client, feature flags, reqwest middleware
references/lightning-method.md
Lightning payment method: charge (BOLT11), session (bearer tokens), Spark SDK
references/custom-methods.md
Custom payment methods: Method.from, Method.toClient, Method.toServer patterns
文件内容
references/protocol-spec.md
核心协议:Challenge/Credential/Receipt结构、状态码、错误处理、安全性、缓存、可扩展性
references/typescript-sdk.md
mppx TypeScript SDK:服务器端/客户端/中间件模式、代理、MCP SDK、CLI、Mppx.create选项
references/tempo-method.md
Tempo支付方式:charge + session意图、费用赞助、推送/拉取模式、自动交换、测试网/主网配置
references/stripe-method.md
Stripe支付方式:SPT流程、服务器端/客户端配置、Stripe Elements、createToken代理、元数据
references/sessions.md
会话意图深度解析:支付通道、凭证签名、SSE流服务、托管合约、充值、关闭
references/transports.md
HTTP和MCP传输绑定:请求头编码、JSON-RPC错误码、对比
references/python-sdk.md
pympp Python SDK:FastAPI/服务器端模式、异步客户端、流会话
references/rust-sdk.md
mpp Rust SDK:服务器端/客户端、特性标志、reqwest中间件
references/lightning-method.md
Lightning支付方式:charge(BOLT11)、session(Bearer令牌)、Spark SDK
references/custom-methods.md
自定义支付方式:Method.from、Method.toClient、Method.toServer模式

Official Resources

官方资源