单次支付#

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 卖家接入#

exactcharge 还是 upto#

exact 单收款方,同步 / 异步结算都支持;默认收稳定币,加 Permit2 可扩展至任意 ERC-20。

charge 除了单收款方,还支持分账(Split),即单次支付多地址收款(≤10),默认且只支持同步结算。

upto 单收款方,价格为上限,调用后按实际用量结算——适合成本调用前算不准的场景。

维度exactchargeupto
收款方单收款方单 / 多地址(≤10)单收款方
金额确定时点调用前固定调用前固定调用后按实际(≤ 上限)
结算时序同步 / 异步默认仅同步同步 / 异步
计价代币EIP-3009 稳定币(加 Permit2 → 任意 ERC-20)EIP-3009 稳定币任意 ERC-20(Permit2)

不确定走哪个?用 同时支持 exact + charge 同时挂上,让买家自己挑。

SDK 状态#

SchemeNode.jsRustGoJavaPython
exact
charge即将推出
upto即将推出即将推出即将推出

exact 的 Permit2 签名与 upto 目前仅 Node.js / Rust 支持,其余语言即将推出。

exact 路径#

每个 Tab 包含完整的安装命令 + 实现代码。架构由 4 个组件组合:Facilitator 客户端(带 OKX API Key)→ Resource Server(注册 scheme)→ Routes 配置accepts 数组)→ 中间件挂载

bash
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
typescript
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" },买家即改签 Permit2scheme 名仍是 exact,路由结构、结算流程(同步 / 异步)都不变——唯一差别是买家首次需对该 token 做一次 approve(授权给 Permit2 合约)。

EIP-3009(默认)Permit2
代币范围仅原生支持 EIP-3009 的稳定币任意 ERC-20
买家首次成本零(纯签名)一次性 approve 给 Permit2 合约
后续支付纯签名approve 后纯签名

package.json

json
{
  "dependencies": {
    "@okxweb3/x402-core": "^0.2",
    "@okxweb3/x402-evm": "^0.2",
    "@okxweb3/x402-express": "^0.2",
    "express": "^4.19"
  }
}
typescript
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,不等确认中等价值,对响应速度有要求

异步结算存在时序风险:资源可能在链上支付最终确认前就已交付。高价值交易建议使用同步结算。charge scheme 默认且只支持同步。

charge 路径#

SDK 状态
Rust / Node.js / Go / Python SDK 已上线;Java SDK 即将推出。

charge 不走 x402 的 accepts 数组,而是用一个 ChargeConfig 类型 + MppCharge<T> extractor 描述每个路由的价格——价格作为类型的关联函数返回,编译期就锁定,不会被路由配置层覆写。

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// 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 合约)。

bash
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
typescript
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:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0"
  }
}
typescript
// 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:

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"
  }
}
typescript
// 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. 1
    安装 OnchainOS Skill

    把以下提示词发给你的 AI Agent,跟随引导完成安装:

    text
    请帮我安装 Onchain OS Payment Skill,让我的 Agent 能生成付款链接并对外收费。
    我的收款钱包地址:0xYourSellerWallet

    详见 Agent 卖家快速开始

  2. 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. 3
    发送付款链接

    Skill 返回的付款 URL(形如 https://pay.okx.com/p/a2a_xxx)作为文本消息发送给买家 Agent,对端解析 URL 后通过 Agentic Wallet 完成签名。

  4. 4
    轮询支付状态

    Skill 自动轮询 GET /payment/{paymentId}/status,状态变 completed 后通知 Agent 交付服务。

    text
    Skill: 支付已完成(tx: 0xabc...)
    Agent: 收到付款,开始翻译...

买家接入#

买家通过给 Agent 安装 Onchain OS Skill 完成接入——安装 Skill 时会自动配置 Agentic Wallet 作为底层签名钱包,无需单独安装。Skill 自动识别 HTTP 402 响应或消息通道里的付款 URL,调用钱包完成签名后重放请求,全程无需买家手动介入。完整接入步骤见 Agent 买家


边界与权衡#

什么时候不该用单次支付
  • 单笔价格极低 + 调用极高频:每笔都走链上结算成本不划算 → 用 批量支付
  • 长期合作、连续多次调用:开通一次通道后所有调用都在通道内累计 → 用 按量支付
  • 单次调用、成本事后才知:用本页 upto 即可;只有"多次调用累计扣费"才需要 按量支付
  • 双方互不信任、需验收后放款:用 担保支付

下一步#