单次支付#
HTTP 卖家 重点看:业务流程 → HTTP 卖家接入(scheme 选型 → exact 路径(含 Permit2)/ charge 路径 / upto 路径 → syncSettle 决策)→ 进阶(分账、同时支持 exact + charge)
Agent 卖家 重点看:业务流程 → Agent 卖家接入(付款链接生成与分发)
定义、底层协议详见 核心概念 · 单次支付。本页聚焦接入。
适用场景#
| 你的业务 | 是否适合 |
|---|---|
| 单笔金额固定明确(一篇报告 / 一次推理 / 一次查询) | ✅ |
| 调用前价格已确定,调用后无后续消费 | ✅ |
| 资源不可撤回(如文件下载、报告生成) | ✅(推荐 syncSettle: true) |
| 单笔成本调用前算不准、需按实际用量结算(如 LLM 推理按 token 计费) | ✅(用 upto) |
业务流程#
下图是抽象层流程——HTTP 卖家是"客户端请求触发",Agent 卖家是"Agent 在对话里主动生成付款链接触发",但Challenge / Credential 消息语义两者一致。具体载体差异见下文卖家接入。
KYT(链上风险审查)是 Broker Verify 阶段内部的合规检查,不是独立服务。
HTTP 卖家接入#
选 exact、charge 还是 upto?#
exact 单收款方,同步 / 异步结算都支持;默认收稳定币,加 Permit2 可扩展至任意 ERC-20。
charge 除了单收款方,还支持分账(Split),即单次支付多地址收款(≤10),默认且只支持同步结算。
upto 单收款方,价格为上限,调用后按实际用量结算——适合成本调用前算不准的场景。
| 维度 | exact | charge | upto |
|---|---|---|---|
| 收款方 | 单收款方 | 单 / 多地址(≤10) | 单收款方 |
| 金额确定时点 | 调用前固定 | 调用前固定 | 调用后按实际(≤ 上限) |
| 结算时序 | 同步 / 异步 | 默认仅同步 | 同步 / 异步 |
| 计价代币 | EIP-3009 稳定币(加 Permit2 → 任意 ERC-20) | EIP-3009 稳定币 | 任意 ERC-20(Permit2) |
不确定走哪个?用 同时支持 exact + charge 同时挂上,让买家自己挑。
SDK 状态#
| Scheme | Node.js | Rust | Go | Java | Python |
|---|---|---|---|---|---|
exact | ✅ | ✅ | ✅ | ✅ | ✅ |
charge | ✅ | ✅ | ✅ | 即将推出 | ✅ |
upto | ✅ | ✅ | 即将推出 | 即将推出 | 即将推出 |
exact的 Permit2 签名与upto目前仅 Node.js / Rust 支持,其余语言即将推出。
exact 路径#
每个 Tab 包含完整的安装命令 + 实现代码。架构由 4 个组件组合:Facilitator 客户端(带 OKX API Key)→ Resource Server(注册 scheme)→ Routes 配置(accepts 数组)→ 中间件挂载。
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
import express from "express";
import {
paymentMiddleware,
x402ResourceServer,
} from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
const app = express();
const NETWORK = "eip155:196";
const PAY_TO = process.env.PAY_TO_ADDRESS || "0xYourSellerWallet";
const facilitatorClient = new OKXFacilitatorClient({
apiKey: "OKX_API_KEY",
secretKey: "OKX_SECRET_KEY",
passphrase: "OKX_PASSPHRASE",
});
const resourceServer = new x402ResourceServer(facilitatorClient);
resourceServer.register(NETWORK, new ExactEvmScheme());
app.use(
paymentMiddleware(
{
"GET /api/premium": {
accepts: [
{
scheme: "exact",
network: NETWORK,
payTo: PAY_TO,
price: "$0.10",
syncSettle: true, // 同步结算:等链上确认
},
],
description: "Premium API",
mimeType: "application/json",
},
},
resourceServer,
),
);
app.get("/api/premium", (_req, res) => {
res.json({ data: "付费资源内容" });
});
app.listen(4000, () => {
console.log("[Seller] listening at http://localhost:4000");
});
多 scheme 声明都通过
accepts: [...]数组完成;要追加aggr_deferred(批量支付)时,在数组里再加一项即可,详见 批量支付。
exact + Permit2:支持任意 ERC-20#
exact 默认走 EIP-3009 签名,只能收原生支持 EIP-3009 的稳定币(USD₮0 / USDG);买家无需链上 approve,仅凭一次链下签名即可付款。
如果你要收 X Layer 上的任意 ERC-20,在 accepts 项里声明一个字段 extra: { assetTransferMethod: "permit2" },买家即改签 Permit2。scheme 名仍是 exact,路由结构、结算流程(同步 / 异步)都不变——唯一差别是买家首次需对该 token 做一次 approve(授权给 Permit2 合约)。
| EIP-3009(默认) | Permit2 | |
|---|---|---|
| 代币范围 | 仅原生支持 EIP-3009 的稳定币 | 任意 ERC-20 |
| 买家首次成本 | 零(纯签名) | 一次性 approve 给 Permit2 合约 |
| 后续支付 | 纯签名 | approve 后纯签名 |
package.json:
{
"dependencies": {
"@okxweb3/x402-core": "^0.2",
"@okxweb3/x402-evm": "^0.2",
"@okxweb3/x402-express": "^0.2",
"express": "^4.19"
}
}
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import { paymentMiddleware, x402ResourceServer } from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
const facilitator = new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const resourceServer = new x402ResourceServer(facilitator)
.register("eip155:196", new ExactEvmScheme());
// The one difference vs plain `exact`:
// extra.assetTransferMethod = "permit2"
// tells the buyer to sign Permit2 instead of EIP-3009. Same scheme name
// ("exact"), same route shape — only the transfer method changes.
const routes = {
"GET /api/premium": {
accepts: {
scheme: "exact",
network: "eip155:196",
payTo: process.env.PAY_TO_ADDRESS!,
price: "$0.05",
extra: { assetTransferMethod: "permit2" },
},
description: "Premium API — paid via Permit2 (any ERC-20 on X Layer)",
mimeType: "application/json",
},
};
const app = express();
app.use(paymentMiddleware(routes, resourceServer));
app.get("/api/premium", (_req: Request, res: Response) => {
res.json({ data: "premium content" });
});
app.listen(4000, async () => {
await resourceServer.initialize();
});
同步结算 vs 异步结算#
调用 /settle 后,Facilitator 将交易提交到链上。路由配置中的 syncSettle 字段控制结算行为:
| 模式 | 参数值 | Facilitator 行为 | 适用场景 |
|---|---|---|---|
| 同步 | true | 提交交易并等待链上确认后返回 txHash | 高价值交易,需确认到账后再交付资源 |
| 异步 | false | 提交交易后立即返回 txHash,不等确认 | 中等价值,对响应速度有要求 |
异步结算存在时序风险:资源可能在链上支付最终确认前就已交付。高价值交易建议使用同步结算。
chargescheme 默认且只支持同步。
charge 路径#
charge 不走 x402 的 accepts 数组,而是用一个 ChargeConfig 类型 + MppCharge<T> extractor 描述每个路由的价格——价格作为类型的关联函数返回,编译期就锁定,不会被路由配置层覆写。
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// 启动: node --env-file=.env --experimental-strip-types server.ts
// 或者: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
// SA-API client (broadcasts EIP-3009 in transaction mode).
const saClient = new SaApiClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const mppx = Mppx.create({
methods: [charge({ saClient })],
realm: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// Per-route price (base units; "100" = 0.0001 of a 6-decimal token).
// fee_payer = true → seller broadcasts the EIP-3009 (transaction mode).
const CHARGE = {
amount: "100",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // receipt
description: "One premium API call",
methodDetails: { chainId: 196, feePayer: true }, // X Layer
} as const;
// Runs only after verify + settle.
async function premium(request: Request): Promise<Response> {
const result = await mppx.charge(CHARGE)(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "premium content" }));
}
// node:http ↔ Web Standards bridge (10 lines).
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/api/premium"
? await premium(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(4000);
upto 路径#
upto 是单次支付的一种变体——买家先授权一个支付上限(cap),服务端完成这次请求后,按真实成本结算实付金额(≤ cap),多授权的部分不扣。如果这次请求最终没有可计费的产出,实付金额可以是 0,此时不会发起任何链上交易。
适合单次调用、但最终金额要等请求执行完才能确定的场景,例如:一次数据查询按实际返回的行数计费、一次批量校验按实际处理成功的条目计费。
和 exact 的关键区别:
price是上限,不是实扣;真实金额由 handler 在请求处理完后回填。upto基于 Permit2 签名(不走 EIP-3009),但结算结构仍是「一次请求一次结算」,不要与按量支付混淆——后者是预存托管 + 跨多次调用累计 Voucher。
upto 基于 Permit2 签名,买家用任意 EVM 钱包即可;与 exact + Permit2 一样,买家首次需对该 token 做一次 approve(授权给 Permit2 合约)。
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
paymentMiddleware,
setSettlementOverrides,
x402ResourceServer,
} from "@okxweb3/x402-express";
import { UptoEvmScheme } from "@okxweb3/x402-evm/upto/server";
const facilitator = new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const resourceServer = new x402ResourceServer(facilitator)
.register("eip155:196", new UptoEvmScheme());
// price 是上限(cap),不是实扣;实扣由 handler 决定。
const routes = {
"GET /api/usage": {
accepts: [
{
scheme: "upto",
network: "eip155:196",
payTo: process.env.PAY_TO_ADDRESS!,
price: "$0.10", // 买家授权的上限
maxTimeoutSeconds: 300,
// extra 省略 —— UptoEvmScheme 自动注入所需字段
},
],
description: "Pay-by-actual-usage demo (upto)",
mimeType: "application/json",
},
};
const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, resourceServer));
app.get("/api/usage", (_req: Request, res: Response) => {
// 算出本次请求的真实成本(如 tokens × 单价)
const actualAmount = "$0.034";
// 中间件读取此 header 按 override 金额结算,并在响应前剥除该 header。
// amount 接受四种格式:
// "1234000" base units
// "50%" 上限的百分比
// "$0.034" 美元字符串(与 price 同语法)
// "0" 短路 —— 不发起链上交易
setSettlementOverrides(res, { amount: actualAmount });
res.json({ report: { tokens_used: 1342, model: "demo" }, billed: actualAmount });
});
app.listen(4000, async () => {
await resourceServer.initialize();
});
upto目前仅 Node.js / Rust SDK 支持;Go / Java / Python 即将推出。
进阶#
仅适用于 HTTP 卖家——下方 SDK 代码块都挂在 HTTP 中间件上;Agent 卖家通过 Skill 生成付款链接,不涉及这一层。
1. 分账(仅 charge)#
一笔支付自动拆给最多 10 个收款方。常见场景:平台抽成、多方分润、推广分佣。
硬约束:
sum(splits.amount) < ChargeConfig::amount()(分账总额必须严格小于主金额)splits.len() ≤ 10- 每个
recipient必须是 EIP-55 校验过的 40-hex 地址
签名负担:买家会为每一路 split 各签一笔 EIP-3009——主收款方 1 笔 + 每个 split 各 1 笔,由卖家在 /settle 时一并提交。
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// 启动: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
const saClient = new SaApiClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const mppx = Mppx.create({
methods: [charge({ saClient })],
realm: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// Total 100 base units; primary keeps 50, splits take 30 + 20.
// Constraints: sum(splits) < amount; splits.length <= 10;
// recipient must be 40-hex EIP-55.
// Buyer signs one EIP-3009 per split (one to primary + one per entry).
const splits = [
{ amount: "30", recipient: "0x....321a1308", memo: "partner-a" },
{ amount: "20", recipient: "0x....d31a6608", memo: "partner-b" },
];
// Only difference vs single charge: methodDetails.splits.
const CHARGE = {
amount: "100",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // primary receipt
description: "One premium API call (split)",
methodDetails: { chainId: 196, feePayer: true, splits },
} as const;
async function premium(request: Request): Promise<Response> {
const result = await mppx.charge(CHARGE)(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "premium content" }));
}
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/api/premium"
? await premium(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(4000);
当前 charge split 落地为显式金额——每一路是绝对 base units,不是比例。
2. 同时支持 exact + charge#
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/payment-router": "^0.1.0",
"@okxweb3/mpp": "^0.1.0",
"@okxweb3/x402-core": "^0.1.0",
"@okxweb3/x402-evm": "^0.1.0"
}
}
// server.ts
// 启动: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge as mppCharge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
x402HTTPResourceServer,
x402ResourceServer,
} from "@okxweb3/x402-core/server";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
import {
MppAdapter,
X402Adapter,
paymentRouter,
} from "@okxweb3/payment-router";
// —— MPP setup ——
const saClient = new SaApiClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const mppx = Mppx.create({
methods: [mppCharge({ saClient })],
realm: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// —— x402 setup (facilitator + scheme; routes are declared on the router) ——
const NETWORK = "eip155:196"; // X Layer Mainnet
const x402Server = new x402ResourceServer(
new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
}),
).register(NETWORK, new ExactEvmScheme());
// Built-in priorities: MPP=10, x402=20 (MPP wins when both headers present).
// Custom adapters should start at priority ≥ 100.
const protect = paymentRouter({
adapters: [
new MppAdapter({ mppx }),
new X402Adapter({
resourceServer: x402Server,
httpResourceServerCtor: x402HTTPResourceServer,
}),
],
routes: {
"GET /generateImg": {
description: "AI Image Generation Service",
adapterConfigs: {
mpp: {
intent: "charge",
amount: "10000",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // receipt
description: "AI Image Generation Service",
methodDetails: { chainId: 196, feePayer: true },
},
x402: {
scheme: "exact",
network: NETWORK,
payTo: "0x...378211", // receipt
price: "$0.01",
description: "AI Image Generation Service",
mimeType: "application/json",
},
},
},
},
});
// Protocol-agnostic. Runs only after one of the adapters has verified payment.
const handler = protect(async () =>
Response.json({
imageUrl: "https://placehold.co/512x512/png?text=AI+Generated",
prompt: "a sunset over mountains",
}),
);
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/generateImg" && req.method === "GET"
? await handler(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(4000);
Agent 卖家接入#
Agent 在对话里主动生成付款链接#
Agent 卖家不是"被动挂中间件等买家请求",而是Agent 在对话里需要收费时主动生成付款链接,发到消息通道(XMTP / Telegram 等)。
本节聚焦付款链接的生成与分发;关于两个 Agent 如何通过 Telegram / XMTP 等消息通道建立会话,详见 快速开始 · 我是 Agent 卖家 — 配置消息通道网关。
- 1安装 OnchainOS Skill
把以下提示词发给你的 AI Agent,跟随引导完成安装:
text请帮我安装 Onchain OS Payment Skill,让我的 Agent 能生成付款链接并对外收费。 我的收款钱包地址:0xYourSellerWallet详见 Agent 卖家快速开始。
- 2生成付款链接
卖家 Agent 在需要收费时调用 Skill 生成一次性付款链接:
text买家Agent:帮我翻译这份 3000 字文档 卖家Agent:好的,翻译服务 50 USD₮0。 [Skill 调用:createPayment({type:'charge', amount:'50000000', recipient:'0x...'})] 付款链接:https://pay.okx.com/p/a2a_01HZX8Q9RK3JWYV7M2N5T8P4AB每条链接是一次性的,默认 30 分钟到期自动失效。
- 3发送付款链接
Skill 返回的付款 URL(形如
https://pay.okx.com/p/a2a_xxx)作为文本消息发送给买家 Agent,对端解析 URL 后通过 Agentic Wallet 完成签名。 - 4轮询支付状态
Skill 自动轮询
GET /payment/{paymentId}/status,状态变completed后通知 Agent 交付服务。textSkill: 支付已完成(tx: 0xabc...) Agent: 收到付款,开始翻译...
买家接入#
买家通过给 Agent 安装 Onchain OS Skill 完成接入——安装 Skill 时会自动配置 Agentic Wallet 作为底层签名钱包,无需单独安装。Skill 自动识别 HTTP 402 响应或消息通道里的付款 URL,调用钱包完成签名后重放请求,全程无需买家手动介入。完整接入步骤见 Agent 买家。
