按量支付#

买家先存一笔到链上托管账户,每次调用从中扣费,用完时把没用的余额退回。 不像单次支付每笔都上链,按量支付只在「开账户」和「结算」时上链,中间无数次调用都在链下完成。 适合 Agent 任务多步调用、长会话聊天、订阅式 API 等需要长期累计扣费的场景。

定义、底层协议详见 核心概念 · 按量支付


适用场景#

你的业务是否适合
业务自然分段、每段单价相同(按调用次数 / 按消息数 / 按子任务数)
长周期、多次累计扣费(订阅式、Agent 任务串联多步调用)
买家希望"用多少算多少",剩余可退

怎么工作#

按量支付分三步走:

步骤一句话
① 预存买家一次性存一笔钱到链上托管账户。这是唯一一次必须上链的动作。
② 边用边签每次调用,买家签一张「截至当前累计消费多少钱」的累计账单(Voucher)(链下、瞬时、0 gas)。卖家本地存着最新一张。
③ 结账卖家任意时刻把手上最新的 Voucher 提交上链,从托管账户里拿走该收的钱;剩余余额自动退回买家。

完整流程(含 retry、追充、强制关闭等失败分支)见下方 完整业务流程


关键术语#

通道(Channel)#

买家预存资金的那个链上托管账户(基于 Escrow 合约托管,谁都不能违反规则挪走)。

Voucher(累计账单)#

买家每次调用时签的那张「截至当前累计消费 X」的累计账单(EIP-712 签名)。

4 个设计性质:

特性说明
累计金额(不是增量)Voucher 写"截至当前累计消费 X"而非"本次扣 Y"
防重放cumulativeAmount 单调递增,旧 Voucher 被链上合约自然拒绝
抗丢失即使中途 Voucher 丢失,只要卖家手上有最新一张,结算时就能拿到完整金额
零上链Voucher 不上链,卖家本地验签即可

为什么用累计而非增量:累计结构同时解决两个问题——一是天然防重放(旧 Voucher 数额必然小于最新一张,链上合约直接拒绝);二是抗丢失(中间几张丢了不影响,最终结算只看最新一张)。

⚠️ 卖家需要把每张 Voucher 持久化保存——具体要求见 持久化保存 Voucher

settle vs. close#

操作通道状态资金流向
settle(中途结算)通道继续开卖家从托管账户拿当前应得,剩余资金继续锁定
close(关闭)通道终态关闭最终结算 + 未使用余额退回买家

宽限期(grace period)#

买家如果想中途单方关闭通道,会触发一个 15 分钟的倒计时窗口,让卖家有时间把手上最新账单上链结算。 这是合约层硬编码的固定值,卖家无需配置。深入细节见 通道生命周期 · 强制关闭


完整业务流程#

读图导引:

  • 阶段 1 —— HTTP 402 Challenge 引导买家完成 EIP-3009 授权 + 链上 open
  • 阶段 2 —— HTTP 402 retry 模型:每次调用都走「请求 → 402 + Challenge → 签 Voucher → 重放」,每张 Voucher 都是单签的 EIP-712,cumulativeAmount 单调递增
  • 阶段 3 —— 三种结算路径并存:中途 settle / 协同 close / 强制 requestClose

通道生命周期#

状态机#

OPEN(正常使用)→ CLOSING(买家发起 requestClose,进入宽限期)→ CLOSED(宽限期结束 / 卖家或买家完成最终结算)。

三种结算路径#

  • 中途 settle:通道保持 OPEN,卖家把当前最新 Voucher 上链 → 合约从托管账户划账 → 剩余资金继续锁定,可继续后续调用。
  • 协同 close:买家配合签最终 Voucher → 卖家 close → 通道终态 CLOSED → 余额退回买家。最干净的关闭路径。
  • 强制 close:见下方「强制关闭」。

强制关闭#

什么是宽限期#

宽限期是 Escrow 合约的延迟关闭保护窗口——买家单方发起强制关闭时,通道不会立即关停,而是先进入 CLOSING 状态倒计时 15 分钟,给卖家用最新 Voucher 抢先结算的机会。 没有这段窗口,买家可以在卖家把最新 Voucher 上链前瞬间关闭通道,让卖家手上那张 Voucher 成为废纸。

15 分钟是合约层硬编码值,卖家无需任何配置。

流程#

买家可单方调 requestClose 触发:

  1. 通道进入宽限期——状态变为 CLOSING,开始 15 分钟倒计时
  2. 宽限期内卖家可抢先 close 用最新 Voucher 结算
  3. 宽限期结束后买家调 withdraw 拿回剩余资金,通道终态为 CLOSED

宽限期固定 15 分钟。买家发起强制关闭后,卖家只有这 15 分钟内将最新 Voucher 上链结算的窗口;窗口过期后,已签未结的 Voucher 金额无法回收。

应对方案:定期主动调用 settle 完成已签 Voucher 的链上结算——例如每 N 次调用触发一次,或每隔几分钟跑一次定时任务。频率越高,单次未结算金额越小,即使错过宽限期,损失也越可控。

持久化保存 Voucher#

⚠️ SDK 默认把最新 Voucher 存在内存里,进程重启即丢失。如果服务器在两次 settle 之间重启,最新 Voucher 丢失后只能用旧的较小那张上链——「最新累计 − 旧累计」之间的金额无法在合约层补救,构成直接资金损失。

必做:把 SDK 的 store 换成持久化存储(Redis / Postgres / SQLite / 文件 store 均可)。SDK 留了 with_store(...) 接口供注入,详见 卖家接入

资金不足时追充#

通道开通后买家可随时调 topUp 增加预存金额;channelId 不变,无需重新 open。Voucher 累计金额可以继续增长。

使用建议
  • 通道长期开启(包月、长期订阅):建议定期发起中途结算,避免持有的 Voucher 价值过高带来风险
  • 业务明确结束(买家取消、会话结束):主动关闭通道让买家及时拿回余额

卖家接入#

SDK 状态#

SchemeNode.jsRustGoJava
session(按量计费通道)即将推出

完整代码#

package.json

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0",
    "viem": "^2.21.0"
  }
}
typescript
// server.ts
// 启动: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { privateKeyToAccount } from "viem/accounts";
import { Mppx } from "@okxweb3/mpp";
import { session } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";

const UNIT_PRICE_BASE_UNITS = "100";   // 0.0001 of a 6-decimal token
const UNIT_TYPE = "request";
const SUGGESTED_DEPOSIT = "10000";     // 100× unit price

const saClient = new SaApiClient({
  apiKey: process.env.OKX_API_KEY!,
  secretKey: process.env.OKX_SECRET_KEY!,
  passphrase: process.env.OKX_PASSPHRASE!,
});

// viem LocalAccount — replace with WalletClient / KMS / HSM signer in production.
// The session method fast-fails on startup if signer.address !== expected payee.
const sellerSigner = privateKeyToAccount(
  process.env.MPP_MERCHANT_PRIVATE_KEY! as `0x${string}`,
);

// Default in-memory store. Pass `store: ...` for SQLite / Redis / Postgres.
const mppx = Mppx.create({
  methods: [session({ saClient, signer: sellerSigner })],
  realm: "test realm",
  secretKey: process.env.MPP_SECRET_KEY!,
});

// Per-route session config. Charged per call; voucher accumulates;
// settle batches on /session/manage close action.
const SESSION = {
  amount: UNIT_PRICE_BASE_UNITS,
  currency: "0x...adb21711",                 // currency
  recipient: "0x...378211",                  // receipt
  description: "Pay-per-use API",
  unitType: UNIT_TYPE,
  suggestedDeposit: SUGGESTED_DEPOSIT,
  methodDetails: {
    chainId: 196,                            // X Layer
    escrowContract: process.env.MPP_ESCROW!, // 40-hex escrow address
    feePayer: true,
    minVoucherDelta: "0",
  },
} as const;

// Routes by `payload.action`: open / voucher / topUp / close.
// mppx.session(...)(request) handles all four uniformly:
//   - 402 → challenge response
//   - 200 → action-specific result; withReceipt() attaches Payment-Receipt
async function manage(request: Request): Promise<Response> {
  const result = await mppx.session(SESSION)(request);
  if (result.status === 402) return result.challenge;
  // open / topUp / close → empty 204; voucher → resource body.
  return result.withReceipt(Response.json({ status: "ok" }));
}

http.createServer(async (req, res) => {
  const url = `http://${req.headers.host ?? "localhost:4023"}${req.url}`;
  const webReq = new Request(url, {
    method: req.method,
    headers: new Headers(req.headers as Record<string, string>),
  });
  const path = new URL(url).pathname;
  const webRes =
    path === "/session/manage"
      ? await manage(webReq)
      : new Response("not found", { status: 404 });
  res.statusCode = webRes.status;
  webRes.headers.forEach((v, k) => res.setHeader(k, v));
  res.end(await webRes.text());
}).listen(4023);

字段说明#

EvmSessionMethod / SessionMethodDetails

字段含义备注
with_escrowEscrow 合约地址必填;通道资金锁在此合约里
with_signer卖家签名器接受任意 alloy::signers::Signer:PrivateKeySigner / AwsSigner / LedgerSigner / 自定义远程签名器
verify_payee启动期校验 signer.address() == 期望收款地址启动失败优于运行期账目错乱
currency计价代币合约地址必填;目前仅支持 USDG / USD₮0 等实现 EIP-3009 的稳定币
recipient主收款地址必填,EIP-55 校验过的 40-hex 地址
chain_id链 ID196 = X Layer
fee_payerSome(true) 卖家代付 gas(transaction 模式)默认推荐 true,让买家无需持有 X Layer gas
min_voucher_delta单张 Voucher 的最小增量(base units)"0" 接受任意;提高数值可降低高频小额 Voucher 的签验开销
unit_type计费单位名自由命名:request / message / subtask
unit_price单价(base units)6 位精度稳定币:"100" = 0.0001
suggested_deposit推荐预存金额(base units)通常设为单价 × 100,覆盖一段会话用量
realm命名空间隔离不同业务线建议用不同 realm,避免凭证串用
secret_key卖家用于签 Challenge 的密钥通过 MPP_SECRET_KEY 环境变量注入,不要硬编码

买家接入#

按你是否装了 Agentic Wallet 分两条路径——4 个动作(open / topUp / 提交 Voucher / close)能力一致,差别在「跟 Agent 用自然语言说」还是「自己写客户端代码」。

路径 A:装了 Agentic Wallet(推荐)#

Agentic Wallet 内置的 Skill 已经知道怎么调这 4 个动作,买家说一句话就能完成:

想做什么跟 Agent 说Skill 实际干什么
开通通道「帮我开通 [服务名] 的按量支付,预存 X」EIP-3009 授权 → Broker 提交 open
调用服务「用 [服务名] 做 X」自动签 Voucher,附在请求里
追加预存「给 [服务名] 通道再加 X 预存」topUp(channelId, amount)
关闭通道「关闭 [服务名] 通道,退回剩余」协同 close(或 requestClose 进入 15 分钟宽限期)

私钥在 TEE 内、Skill 自动签名,X Layer 上 USDG / USD₮0 由 OKX 代付——0 gas。

路径 B:用普通 EVM 钱包#

任意支持 EIP-712 签名和 EIP-3009 的钱包都能用,但开发者要自己实现 4 个动作的客户端代码——open / voucher / topUp / close 全部走 HTTP 402 challenge retry 模型,每次都把签好的凭证塞进同一个请求头:

Authorization: Payment <base64url(JCS-canonicalized JSON envelope)>

签名构造(EIP-712 Voucher / EIP-3009 transferWithAuthorization typed data 详情)见 协议规范

以下是代码示例:

① open(开通通道)#

业务请求 curl:

bash
curl -i 'https://api.example.com/v1/chat/completions' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJvcGVuIiwiYXV0aG9yaXphdGlvbiI6eyJmcm9tIjoiMHgxMjM0NTY3ODkwYUJjRGVmMTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4Iiwibm9uY2UiOiIweGExYjJjM2Q0ZTVmNjA3MTgyOTNhNGI1YzZkN2U4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOTAiLCJ0byI6IjB4ZmYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMSIsInR5cGUiOiJlaXAtMzAwOSIsInZhbGlkQWZ0ZXIiOiIwIiwidmFsaWRCZWZvcmUiOiIxNzQ2NjE5MjAwIiwidmFsdWUiOiIxMDAwMDAifSwiY2hhbm5lbElkIjoiMHg2ZDBmNGZkZjFmMmY2YTFmNmMxYjBmYmQ2YTdkNWMyYzBhOGQzZDdiMWY2YTljMWIzZTJkNGE1YjZjN2Q4ZTlmIiwiY3VtdWxhdGl2ZUFtb3VudCI6IjAiLCJzYWx0IjoiMHg5YThiN2M2ZDVlNGYzYTJiMWMwZDllOGY3YTZiNWM0ZDNlMmYxYTBiOWM4ZDdlNmY1YTRiM2MyZDFlMGY5YThiIiwic2lnbmF0dXJlIjoiMHhhYjAxY2QyM2VmNDU2Nzg5MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjFjIiwidHlwZSI6InRyYW5zYWN0aW9uIiwidm91Y2hlclNpZ25hdHVyZSI6IjB4MTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzMWIifSwic291cmNlIjoiZGlkOnBraDplaXAxNTU6MTk2OjB4MTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4OTBhQmNEZWYxMjM0NTY3OCJ9'

解码后 envelope 明文:

jsonc
{
  "challenge": {
    // 完整回显卖家 WWW-Authenticate 的 6 个字段
    "id": "kM9xPqWvT2nJrHsY4aDfEb",        // 服务端本次 challenge 唯一标识
    "realm": "api.example.com",            // 卖家域,HMAC 校验范围
    "method": "evm",                       // EVM 链支付方法
    "intent": "session",                   // session ↔ charge 二选一
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",  // 卖家原始请求参数(amount/currency/methodDetails…)的 base64url
    "expires": "2026-05-07T12:00:00Z"      // 过期时间(RFC3339)
  },
  "source": "did:pkh:eip155:196:0x1234567890aBcDef1234567890aBcDef12345678",
                                           // 付款人 DID,格式固定 did:pkh:eip155:<chainId>:<addr>
  "payload": {
    "action": "open",                      // 阶段判别 —— open/voucher/topUp/close 之一
    "type": "transaction",                 // transaction = 卖家代付 gas;hash = 客户端自己已广播
    "channelId": "0x6d0f4fdf...e9f",       // 链上确定性派生,open 时计算出的
    "salt": "0x9a8b...9a8b",                // channelId 派生用的随机salt
    "authorization": {                     // EIP-3009 充值授权(transaction 模式独有)
      "type": "eip-3009",                  // 固定值
      "from": "0x1234...5678",             // source 里的 addr
      "to": "0xff00...0001",               // escrow 合约
      "value": "100000",                   // 充值金额,atomic units(USDC 100000 = 0.1 USDC)
      "validAfter": "0",
      "validBefore": "1746619200",         // EIP-3009 签名有效期 unix 秒
      "nonce": "0xa1b2...8f90"              // EIP-3009 nonce,按合约公式派生
    },
    "signature": "0xab01...ef1c",          // 上面 authorization 的 EIP-3009 签名
    "cumulativeAmount": "0",               // voucher 的累计值,默认 "0"
    "voucherSignature": "0x1122...331b"    // Voucher(channelId, cum=0) 的 EIP-712 签名
  }
}

② voucher(每次业务请求扣费)#

业务请求 curl:

bash
curl -i 'https://api.example.com/v1/chat/completions' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJ2b3VjaGVyIiwiY2hhbm5lbElkIjoiMHg2ZDBmNGZkZjFmMmY2YTFmNmMxYjBmYmQ2YTdkNWMyYzBhOGQzZDdiMWY2YTljMWIzZTJkNGE1YjZjN2Q4ZTlmIiwiY3VtdWxhdGl2ZUFtb3VudCI6IjUwMDAiLCJzaWduYXR1cmUiOiIweGRlYWRiZWVmMDAxMTIyMzM0NDU1NjY3Nzg4OTkwMGFhYmJjY2RkZWVmZjAwMTEyMjMzNDQ1NTY2Nzc4ODk5YWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTlhYWJiY2NkZGVlZmYwMDExMjIzMzQ0NTU2Njc3ODg5OTExYiJ9fQ'

解码后 envelope 明文:

jsonc
{
  "challenge": {
    // 与 open 相同结构,回显本次 voucher challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "payload": {
    "action": "voucher",                    // 阶段判别 —— voucher
    "channelId": "0x6d0f4fdf...e9f",        // 必须是 open 时拿到的同一个 channelId
    "cumulativeAmount": "5000",             // 累计值(atomic units),必须严格大于上一张
    "signature": "0xdeadbeef...11b"         // EIP-712 Voucher(channelId, cum) 的签名
  }
}

③ topUp(追加预存)#

业务请求 curl:

bash
curl -i -X POST 'https://api.example.com/session/manage' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNU0lzSW1abFpWQmhlV1Z5SWpwMGNuVmxmWDAifSwicGF5bG9hZCI6eyJhY3Rpb24iOiJ0b3BVcCIsImFkZGl0aW9uYWxEZXBvc2l0IjoiNTAwMDAiLCJhdXRob3JpemF0aW9uIjp7ImZyb20iOiIweDEyMzQ1Njc4OTBhQmNEZWYxMjM0NTY3ODkwYUJjRGVmMTIzNDU2NzgiLCJub25jZSI6IjB4YzNkNGU1ZjYwNzE4MjkzYTRiNWM2ZDdlOGY5MDAxYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOTAwMSIsInRvIjoiMHhmZjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxIiwidHlwZSI6ImVpcC0zMDA5IiwidmFsaWRBZnRlciI6IjAiLCJ2YWxpZEJlZm9yZSI6IjE3NDY2MTk1MDAiLCJ2YWx1ZSI6IjUwMDAwIn0sImNoYW5uZWxJZCI6IjB4NmQwZjRmZGYxZjJmNmExZjZjMWIwZmJkNmE3ZDVjMmMwYThkM2Q3YjFmNmE5YzFiM2UyZDRhNWI2YzdkOGU5ZiIsInNpZ25hdHVyZSI6IjB4ZmVlZGZhY2UxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxYiIsInRvcFVwU2FsdCI6IjB4YjFjMmQzZTRmNWE2MDcxODI5MzA0MTUyNjM3NDg1OTZhN2I4YzlkMGUxZjIwMzE0MjUzNjQ3NTg2OTcwOGE5YiIsInR5cGUiOiJ0cmFuc2FjdGlvbiJ9LCJzb3VyY2UiOiJkaWQ6cGtoOmVpcDE1NToxOTY6MHgxMjM0NTY3ODkwYUJjRGVmMTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4In0'

解码后 envelope 明文:

jsonc
{
  "challenge": {
    // 与 open 相同结构,回显本次 topup challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "source": "did:pkh:eip155:196:0x1234567890aBcDef1234567890aBcDef12345678",
  "payload": {
    "action": "topUp",                     // 阶段判别
    "type": "transaction",                 // transaction = 卖家代付;hash = 客户端已自行广播
    "channelId": "0x6d0f4fdf...e9f",       // 必须是 open 那个 channelId
    "topUpSalt": "0xb1c2...0a9b",          // 32-byte 随机 salt,用于派生 EIP-3009 nonce
    "authorization": {                     // EIP-3009 充值授权
      "type": "eip-3009",                  // 固定值
      "from": "0x1234...5678",             // 付款人
      "to": "0xff00...0001",               // escrow 合约
      "value": "50000",                    // = additionalDeposit(atomic units,必须一致)
      "validAfter": "0",
      "validBefore": "1746619500",         // 签名有效期 unix 秒
      "nonce": "0xc3d4...9001"              // 上面公式派生的 nonce(不能随机)
    },
    "signature": "0xfeed...001b",          // 上面 authorization 的 EIP-3009 签名
    "additionalDeposit": "50000"            // 本次追加抵押额(必须等于 authorization.value)
  }
}

④ close(关闭通道)#

业务请求 curl:

bash
curl -i -X POST 'https://api.example.com/session/manage' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJjbG9zZSIsImNoYW5uZWxJZCI6IjB4NmQwZjRmZGYxZjJmNmExZjZjMWIwZmJkNmE3ZDVjMmMwYThkM2Q3YjFmNmE5YzFiM2UyZDRhNWI2YzdkOGU5ZiIsImN1bXVsYXRpdmVBbW91bnQiOiI0MjAwMCIsInNpZ25hdHVyZSI6IjB4Y2FmZWJhYmU5OTg4Nzc2NjU1NDQzMzIyMTEwMDk5YWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTlhYWJiY2NkZGVlZmYwMDExMjIzMzQ0NTU2Njc3ODg5OWFhYmJjY2RkZWVmZjAwMTEyMjMzNDQ1NTY2Nzc4ODk5MjFjIn19'

解码后 envelope 明文:

jsonc
{
  "challenge": {
    // 同上,回显卖家最后一次 challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "payload": {
    "action": "close",                      // 阶段判别 —— close
    "channelId": "0x6d0f4fdf...e9f",        // 必须是 open 那个 channelId
    "cumulativeAmount": "42000",            // = current_cum,session 内最高累计值
    "signature": "0xcafebabe...21c"         // EIP-712 final Voucher 签名
  }
}

Gas 费#

X Layer 上 USDG / USD₮0 的 gas 由 OKX 承担——两条路径都 0 gas。


边界与权衡#

什么时候不该用按量支付
  • 价格固定且已知:单次支付 直接匹配;按量支付的预存 + 累计模型用不上
  • 单价极低 + 超高频:批量支付 的 Session Key + TEE 模型为此场景专门优化
  • 买家只调用一两次:按量支付假设持续合作关系;一次性调用直接用 单次支付
  • 需要担保释放(任务交付前不放款):用 担保支付

Agent 卖家(即将推出)#

Agent 卖家版本即将推出。Agent 卖家场景由 OKX 在协议基础上扩展承载(与 HTTP 卖家在底层独立),但语义层(Challenge / Credential)和字段结构与 HTTP 卖家一致。

维度HTTP 卖家Agent 卖家(即将推出)
Challenge 载体HTTP 402 响应消息通道消息体
通道开通触发客户端首次请求Agent 在对话里主动发起
Voucher 提交HTTP 请求头消息回复
业务驱动API 调用Agent 对话

下一步#