Node SDK 参考#
Node SDK 参考(适用于 exact、aggr_deferred)#
包#
| 包 | 描述 |
|---|---|
@okxweb3/x402-core | 核心:客户端、服务端、facilitator、类型 |
@okxweb3/x402-evm | EVM 机制:exact、aggr_deferred |
@okxweb3/x402-express | Express 中间件(卖方) |
@okxweb3/x402-next | Next.js 中间件(卖方) |
@okxweb3/x402-hono | Hono 中间件(卖方) |
@okxweb3/x402-fastify | Fastify 中间件(卖方) |
@okxweb3/x402-fetch | Fetch 包装器(买方) |
@okxweb3/x402-axios | Axios 包装器(买方) |
@okxweb3/x402-mcp | MCP 集成 |
@okxweb3/x402-paywall | 浏览器付费墙 UI |
@okxweb3/x402-extensions | 协议扩展 |
核心类型#
Network#
type Network = `${string}:${string}`;
// CAIP-2 format, e.g., "eip155:196", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
Money / Price / AssetAmount#
type Money = string | number;
// User-friendly amount, e.g., "$0.01", "0.01", 0.01
type AssetAmount = {
asset: string; // Token contract address
amount: string; // Amount in token's smallest unit (e.g., "10000" for 0.01 USDC)
extra?: Record<string, unknown>; // Scheme-specific data (e.g., EIP-712 domain)
};
type Price = Money | AssetAmount;
// Either a user-friendly amount or a specific token amount
ResourceInfo#
interface ResourceInfo {
url: string; // Resource URL path
description?: string; // Human-readable description
mimeType?: string; // Response content type (e.g., "application/json")
}
PaymentRequirements#
描述卖方接受的支付方式。
type PaymentRequirements = {
scheme: string; // Payment scheme: "exact" | "aggr_deferred"
network: Network; // CAIP-2 network identifier
asset: string; // Token contract address
amount: string; // Price in token's smallest unit
payTo: string; // Recipient wallet address
maxTimeoutSeconds: number; // Payment authorization validity window
extra: Record<string, unknown>; // Scheme-specific data
};
各 scheme 的 extra 字段:
| Scheme | Extra 字段 | Type | 描述 |
|---|---|---|---|
exact (EIP-3009) | extra.eip712.name | string | EIP-712 域名称(例如 "USD Coin") |
exact (EIP-3009) | extra.eip712.version | string | EIP-712 域版本(例如 "2") |
PaymentRequired#
发送给客户端的 HTTP 402 响应体。
type PaymentRequired = {
x402Version: number; // Protocol version (currently 2)
error?: string; // Optional error message
resource: ResourceInfo; // Protected resource metadata
accepts: PaymentRequirements[]; // List of accepted payment options
extensions?: Record<string, unknown>; // Extension data (e.g., Bazaar)
};
PaymentPayload#
客户端在重试请求中提交的签名支付。
type PaymentPayload = {
x402Version: number; // Must match server's version
resource?: ResourceInfo; // Optional resource reference
accepted: PaymentRequirements; // The chosen payment option from `accepts`
payload: Record<string, unknown>; // Scheme-specific signed data (see below)
extensions?: Record<string, unknown>; // Extension data
};
各 scheme 的 payload 字段:
exact (EIP-3009) 的 payload:
{
signature: `0x${string}`; // EIP-712 signature
authorization: {
from: `0x${string}`; // Buyer wallet address
to: `0x${string}`; // Seller wallet address
value: string; // Amount in smallest unit
validAfter: string; // Unix timestamp (start validity)
validBefore: string; // Unix timestamp (end validity)
nonce: `0x${string}`; // 32-byte unique nonce
};
}
aggr_deferred 的 payload:
{
signature: `0x${string}`; // Session key signature
authorization: { /* same as EIP-3009 */ };
// acceptedExtraOverrides includes sessionCert
}
VerifyResponse#
type VerifyResponse = {
isValid: boolean; // Whether signature is valid
invalidReason?: string; // Machine-readable reason code
invalidMessage?: string; // Human-readable error message
payer?: string; // Recovered payer address
extensions?: Record<string, unknown>;
};
SettleResponse#
type SettleResponse = {
success: boolean; // Whether settlement succeeded
status?: "pending" | "success" | "timeout"; // OKX extension
errorReason?: string; // Machine-readable error code
errorMessage?: string; // Human-readable error message
payer?: string; // Payer address
transaction: string; // On-chain transaction hash (empty for aggr_deferred)
network: Network; // Settlement network
amount?: string; // Actual settled amount (may differ for "upto")
extensions?: Record<string, unknown>;
};
SupportedKind / SupportedResponse#
type SupportedKind = {
x402Version: number;
scheme: string;
network: Network;
extra?: Record<string, unknown>;
};
type SupportedResponse = {
kinds: SupportedKind[];
extensions: string[]; // Supported extension keys
signers: Record<string, string[]>; // CAIP family → signer addresses
};
服务端 API (x402ResourceServer)#
构造函数#
import { x402ResourceServer } from "@okxweb3/x402-core/server";
const server = new x402ResourceServer(facilitatorClients?);
// facilitatorClients: FacilitatorClient | FacilitatorClient[]
register(network, server)#
注册服务端 scheme。可链式调用。
server
.register("eip155:84532", new ExactEvmScheme())
.register("eip155:196", new AggrDeferredEvmScheme());
registerExtension(extension)#
interface ResourceServerExtension {
key: string;
enrichDeclaration?: (declaration: unknown, transportContext: unknown) => unknown;
enrichPaymentRequiredResponse?: (
declaration: unknown,
context: PaymentRequiredContext,
) => Promise<unknown>;
enrichSettlementResponse?: (
declaration: unknown,
context: SettleResultContext,
) => Promise<unknown>;
}
initialize()#
从 facilitator 获取支持的 kinds。在启动时调用一次。
await server.initialize();
buildPaymentRequirements(config) → PaymentRequirements[]#
interface ResourceConfig {
scheme: string; // "exact" | "aggr_deferred" | "upto"
payTo: string; // Recipient wallet address
price: Price; // "$0.01" or AssetAmount
network: Network; // "eip155:196"
maxTimeoutSeconds?: number; // Default: 300
extra?: Record<string, unknown>;
}
const reqs = await server.buildPaymentRequirements({
scheme: "exact",
payTo: "0xSeller",
price: "$0.01",
network: "eip155:196",
});
buildPaymentRequirementsFromOptions(options, context) → PaymentRequirements[]#
动态定价和 payTo。函数接收 context 参数。
const reqs = await server.buildPaymentRequirementsFromOptions(
[
{
scheme: "exact",
network: "eip155:196",
payTo: (ctx) => ctx.sellerId === "A" ? "0xWalletA" : "0xWalletB",
price: (ctx) => ctx.premium ? "$0.10" : "$0.01",
},
],
requestContext
);
verifyPayment(payload, requirements) → VerifyResponse#
const result = await server.verifyPayment(paymentPayload, requirements);
// result.isValid: boolean
settlePayment(payload, requirements, ...) → SettleResponse#
const result = await server.settlePayment(
paymentPayload,
requirements,
declaredExtensions?, // Extension data from 402 response
transportContext?, // HTTP transport context
settlementOverrides?, // { amount: "$0.05" } for upto scheme
);
服务端生命周期钩子#
| 钩子 | 上下文 | 可中止/恢复 |
|---|---|---|
onBeforeVerify | { paymentPayload, requirements } | { abort: true, reason, message? } |
onAfterVerify | { paymentPayload, requirements, result } | 否 |
onVerifyFailure | { paymentPayload, requirements, error } | { recovered: true, result } |
onBeforeSettle | { paymentPayload, requirements } | { abort: true, reason, message? } |
onAfterSettle | { paymentPayload, requirements, result, transportContext? } | 否 |
onSettleFailure | { paymentPayload, requirements, error } | { recovered: true, result } |
server.onBeforeVerify(async (ctx) => {
// Log or gate verification
});
server.onAfterSettle(async (ctx) => {
console.log(`Settled: ${ctx.result.transaction} on ${ctx.result.network}`);
});
server.onSettleFailure(async (ctx) => {
if (ctx.error.message.includes("timeout")) {
return { recovered: true, result: { success: true, transaction: "", network: "eip155:196" } };
}
});
HTTP 资源服务器 (x402HTTPResourceServer)#
更高层的封装,处理路由匹配、付费墙和 HTTP 特定逻辑。
构造函数#
import { x402HTTPResourceServer } from "@okxweb3/x402-core/http";
const httpServer = new x402HTTPResourceServer(resourceServer, routes);
RoutesConfig#
type RoutesConfig = Record<string, RouteConfig> | RouteConfig;
interface RouteConfig {
accepts: PaymentOption | PaymentOption[]; // Accepted payment methods
resource?: string; // Override resource name
description?: string; // Human-readable description
mimeType?: string; // Response MIME type
customPaywallHtml?: string; // Custom HTML for browser 402 page
unpaidResponseBody?: (ctx: HTTPRequestContext) => HTTPResponseBody | Promise<HTTPResponseBody>;
settlementFailedResponseBody?: (ctx, result) => HTTPResponseBody | Promise<HTTPResponseBody>;
extensions?: Record<string, unknown>;
}
interface PaymentOption {
scheme: string; // "exact" | "aggr_deferred" | "upto"
payTo: string | DynamicPayTo; // Static or dynamic recipient
price: Price | DynamicPrice; // Static or dynamic price
network: Network;
maxTimeoutSeconds?: number;
extra?: Record<string, unknown>;
}
// Dynamic functions receive HTTPRequestContext
type DynamicPayTo = (context: HTTPRequestContext) => string | Promise<string>;
type DynamicPrice = (context: HTTPRequestContext) => Price | Promise<Price>;
onSettlementTimeout(hook)#
type OnSettlementTimeoutHook = (txHash: string, network: string) => Promise<{ confirmed: boolean }>;
httpServer.onSettlementTimeout(async (txHash, network) => {
// Custom recovery logic
return { confirmed: false };
});
onProtectedRequest(hook)#
type ProtectedRequestHook = (
context: HTTPRequestContext,
routeConfig: RouteConfig,
) => Promise<void | { grantAccess: true } | { abort: true; reason: string }>;
httpServer.onProtectedRequest(async (ctx, config) => {
// Grant free access for certain users
if (ctx.adapter.getHeader("x-api-key") === "internal") {
return { grantAccess: true };
}
});
中间件参考#
Express (@okxweb3/x402-express)#
import {
paymentMiddleware,
paymentMiddlewareFromConfig,
paymentMiddlewareFromHTTPServer,
setSettlementOverrides,
} from "@okxweb3/x402-express";
// From pre-configured server (recommended)
app.use(paymentMiddleware(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?));
// From config (creates server internally)
app.use(paymentMiddlewareFromConfig(routes, facilitatorClients?, schemes?, paywallConfig?, paywall?, syncFacilitatorOnStart?));
// From HTTP server (most control)
app.use(paymentMiddlewareFromHTTPServer(httpServer, paywallConfig?, paywall?, syncFacilitatorOnStart?));
// Settlement override in handler (for "upto" scheme)
app.post("/api/generate", (req, res) => {
setSettlementOverrides(res, { amount: "$0.05" });
res.json({ result: "..." });
});
| Parameter | Type | Default | 描述 |
|---|---|---|---|
routes | RoutesConfig | 必填 | 路由到支付配置的映射 |
server | x402ResourceServer | 必填 | 预配置的资源服务器 |
paywallConfig | PaywallConfig | undefined | 浏览器付费墙设置 |
paywall | PaywallProvider | undefined | 自定义付费墙渲染器 |
syncFacilitatorOnStart | boolean | true | 在首次请求时获取支持的 kinds |
Next.js (@okxweb3/x402-next)#
import {
paymentProxy,
paymentProxyFromConfig,
paymentProxyFromHTTPServer,
withX402,
withX402FromHTTPServer,
} from "@okxweb3/x402-next";
// As global middleware (middleware.ts)
const proxy = paymentProxy(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);
export async function middleware(request: NextRequest) { return proxy(request); }
export const config = { matcher: ["/api/:path*"] };
// Per-route wrapper (app/api/data/route.ts)
export const GET = withX402(handler, routeConfig, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);
export const GET = withX402FromHTTPServer(handler, httpServer, paywallConfig?, paywall?, syncFacilitatorOnStart?);
Hono (@okxweb3/x402-hono)#
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-hono";
app.use("/*", paymentMiddleware(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?));
Fastify (@okxweb3/x402-fastify)#
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-fastify";
// NOTE: Fastify registers hooks directly, returns void
paymentMiddleware(app, routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);
EVM 机制类型#
ExactEvmScheme(服务端)#
import { ExactEvmScheme } from "@okxweb3/x402-evm";
const scheme = new ExactEvmScheme(); // No constructor args for server-side
scheme.scheme; // "exact"
// Automatically handles price parsing, EIP-712 domain injection
AggrDeferredEvmScheme(服务端)#
import { AggrDeferredEvmScheme } from "@okxweb3/x402-evm/deferred/server";
const scheme = new AggrDeferredEvmScheme();
scheme.scheme; // "aggr_deferred"
// Delegates to ExactEvmScheme for price parsing
客户端 API(买方)#
买方包会自动处理 402 Payment Required 响应:解析支付要求 → 通过配置的 EVM scheme 签名支付 payload → 携带 PAYMENT 头重发请求。
按你已用的 HTTP 客户端选择对应包:
| 包 | 包装对象 | 适用场景 |
|---|---|---|
@okxweb3/x402-axios | AxiosInstance | 已有 Axios 代码库;需要使用拦截器 / 实例配置 |
@okxweb3/x402-fetch | globalThis.fetch | 基于 fetch 的运行时(浏览器、Edge、Node 18+) |
两者的 API 形态一致:wrapXxxWithPayment(client_or_fetch, x402Client) 与 wrapXxxWithPaymentFromConfig(client_or_fetch, config)。
Axios — @okxweb3/x402-axios#
npm install @okxweb3/x402-axios @okxweb3/x402-evm @okxweb3/x402-core axios
import axios from "axios";
import { wrapAxiosWithPaymentFromConfig } from "@okxweb3/x402-axios";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { xLayer } from "viem/chains";
// 用买家私钥构造 viem signer
const signer = toClientEvmSigner(
createWalletClient({
account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`),
chain: xLayer,
transport: http(),
}),
);
const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
schemes: [
{
network: "eip155:196", // X Layer;用 "eip155:*" 可匹配任意 EVM 链
client: new ExactEvmScheme(signer),
},
],
});
// 402 → 签名 → 重发,对调用方完全透明
const response = await api.get("https://api.example.com/paid-endpoint");
Fetch — @okxweb3/x402-fetch#
npm install @okxweb3/x402-fetch @okxweb3/x402-evm @okxweb3/x402-core
import { wrapFetchWithPaymentFromConfig } from "@okxweb3/x402-fetch";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { xLayer } from "viem/chains";
const signer = toClientEvmSigner(
createWalletClient({
account: privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`),
chain: xLayer,
transport: http(),
}),
);
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{
network: "eip155:196",
client: new ExactEvmScheme(signer),
},
],
});
const response = await fetchWithPayment("https://api.example.com/paid-endpoint");
使用 x402Client 的 builder 模式#
需要注册多个 scheme / 网络,或在多个 transport 间共享同一客户端时,使用显式 builder。
import axios from "axios";
import { wrapAxiosWithPayment, x402Client } from "@okxweb3/x402-axios";
import { ExactEvmScheme, toClientEvmSigner } from "@okxweb3/x402-evm";
const client = new x402Client()
.register("eip155:196", new ExactEvmScheme(signer));
const api = wrapAxiosWithPayment(axios.create(), client);
x402Client 也由 @okxweb3/x402-fetch 重新导出,同一实例可同时供两种 transport 使用。
读取支付回执#
请求重发成功后,服务端会在响应头返回 PAYMENT-RESPONSE,包含链上回执(txHash、实际结算金额等)。用 decodePaymentResponseHeader 解码:
import { decodePaymentResponseHeader } from "@okxweb3/x402-axios"; // 或 "@okxweb3/x402-fetch"
// Axios
const paymentResponse = response.headers["payment-response"];
// Fetch
// const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
if (paymentResponse) {
const receipt = decodePaymentResponseHeader(paymentResponse);
console.log("支付回执:", receipt);
}
x402ClientConfig#
| 字段 | 类型 | 描述 |
|---|---|---|
schemes | SchemeRegistration[] | 必填。每一项把 network(如 "eip155:196"、"eip155:*")与 scheme 客户端(如 new ExactEvmScheme(signer))配对。 |
policies | PaymentPolicy[] | 可选。见下方 Policies,按顺序对 accepts 进行过滤 / 转换。 |
paymentRequirementsSelector | SelectPaymentRequirements | 可选。从过滤后的列表中挑选最终一项。默认 (version, accepts) => accepts[0]。 |
选择流水线(Selection Pipeline)#
收到 402 后,客户端按三步决定签哪一项:
- 按已注册的 scheme 过滤 —— 只保留
network+scheme都已通过register()注册的accepts。 - 按顺序应用 policies —— 每个
PaymentPolicy进一步过滤 / 转换列表。 - selector 选择 —— 从过滤后的列表中选出最终要签名的那一项。
第 1 或第 2 步留空数组时客户端直接抛错——不会发起签名。
Policies — PaymentPolicy#
type PaymentPolicy = (
x402Version: number,
paymentRequirements: PaymentRequirements[],
) => PaymentRequirements[];
policy 是个纯函数:拿当前 accepts,返回过滤后的子集(或转换后的副本)。常用场景:金额上限、网络白名单、scheme 偏好等。
import {
wrapAxiosWithPaymentFromConfig,
type PaymentPolicy,
} from "@okxweb3/x402-axios";
// 拒绝单笔超过 1 USDT(1_000_000 原子单位,6 位小数)的支付
const maxAmountPolicy: PaymentPolicy = (_version, reqs) =>
reqs.filter(r => BigInt(r.amount) <= 1_000_000n);
// 仅允许 X Layer 主网
const xLayerOnlyPolicy: PaymentPolicy = (_version, reqs) =>
reqs.filter(r => r.network === "eip155:196");
// 同时提供两种 scheme 时优先选 "exact"
const preferExactPolicy: PaymentPolicy = (_version, reqs) => {
const exact = reqs.filter(r => r.scheme === "exact");
return exact.length > 0 ? exact : reqs;
};
const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
schemes: [{ network: "eip155:196", client: new ExactEvmScheme(signer) }],
policies: [maxAmountPolicy, xLayerOnlyPolicy, preferExactPolicy],
});
policies 按数组顺序运行,把"收紧"型 policy(金额上限、白名单)放前面,"偏好"型 policy 放后面。
自定义 selector — SelectPaymentRequirements#
type SelectPaymentRequirements = (
x402Version: number,
paymentRequirements: PaymentRequirements[],
) => PaymentRequirements;
selector 在 policies 之后运行。当过滤后仍剩多项需要明确的挑选逻辑(如选最便宜)时使用:
const cheapestFirst: SelectPaymentRequirements = (_version, reqs) =>
[...reqs].sort((a, b) => Number(BigInt(a.amount) - BigInt(b.amount)))[0];
const api = wrapAxiosWithPaymentFromConfig(axios.create(), {
schemes: [{ network: "eip155:*", client: new ExactEvmScheme(signer) }],
paymentRequirementsSelector: cheapestFirst,
});
生命周期钩子(Lifecycle Hooks)#
x402Client 提供三个生命周期钩子,用于埋点、最后一道闸门、错误恢复。用 builder 形式注册:
import { wrapAxiosWithPayment, x402Client } from "@okxweb3/x402-axios";
import { ExactEvmScheme } from "@okxweb3/x402-evm";
const client = new x402Client()
.register("eip155:196", new ExactEvmScheme(signer))
// 1. 签名前 —— 可整体中止支付
.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
const tooExpensive = BigInt(selectedRequirements.amount) > 5_000_000n;
if (tooExpensive) {
return { abort: true, reason: "金额超出买家策略上限" };
}
})
// 2. 签名成功后 —— 仅观察(日志、埋点)
.onAfterPaymentCreation(async ({ paymentPayload }) => {
console.log("已签名 payload nonce:", paymentPayload.payload?.authorization?.nonce);
})
// 3. 签名失败时 —— 可返回手动构造的 payload 进行恢复
.onPaymentCreationFailure(async ({ error }) => {
console.error("payment 创建失败:", error.message);
// return { recovered: true, payload: fallbackPayload };
});
const api = wrapAxiosWithPayment(axios.create(), client);
| 钩子 | 触发时机 | 返回语义 |
|---|---|---|
onBeforePaymentCreation | 选择完成、scheme 签名前 | 返回 void 继续 · 返回 { abort: true, reason } 取消并 reject |
onAfterPaymentCreation | scheme 返回签名后的 payload 后 | 仅 void(仅观察,不可改) |
onPaymentCreationFailure | scheme 签名时抛错 | 返回 void 继续抛错 · 返回 { recovered: true, payload } 用替代 payload 恢复 |
同一阶段内的钩子按注册顺序执行。
客户端扩展 — registerExtension#
当 PaymentRequired 响应里带 extensions 字段、需要 scheme 相关的 payload 增强(如 Gas 代付的 permit 签名)时使用。enrichPaymentPayload 只会在 paymentRequired.extensions 中包含同名 key 时被触发。
client.registerExtension({
key: "eip2612GasSponsoring",
async enrichPaymentPayload(payload, paymentRequired) {
// 签 EIP-2612 permit,挂在 payload.extensions 上
return { ...payload, extensions: { ...payload.extensions, /* ... */ } };
},
});
Node SDK 参考(适用于 charge、session)#
安装与导入#
npm install @okxweb3/mpp viem
@okxweb3/mpp 自带上游 mppx 全部命名空间的透传,应用代码通常只需 import 这一个包。viem 用于 session 的 EIP-712 签名(SessionSigner),charge 不需要。
// 顶层:mppx runtime + 命名空间
import { Mppx, Errors } from '@okxweb3/mpp'
// EVM 共用:SA API 客户端、EIP-712 工具
import { SaApiClient, verifyVoucher, buildSettleAuth } from '@okxweb3/mpp/evm'
// EVM 服务端工厂
import { charge, session } from '@okxweb3/mpp/evm/server'
Charge - 单次支付#
注册#
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: 'demo.merchant.com',
secretKey: process.env.MPPX_SECRET_KEY!,
})
调用#
async function premium(request: Request): Promise<Response> {
const result = await mppx.charge({
amount: '100',
currency: '0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035',
recipient: '0x742d35Cc6634c0532925a3b844bC9e7595F8fE00',
methodDetails: { feePayer: true }, // chainId 默认 196
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: 'premium content' }))
}
调用选项#
type ChargeOptions = {
amount: string // 扣费金额,base units 整数字符串
currency: string // ERC-20 合约 EVM 地址
recipient: string // 收款方 EVM 地址
description?: string // 描述,写入 challenge
externalId?: string // 商户订单号,回带在 receipt 上
methodDetails: {
chainId?: number // 默认 196 (X Layer)
feePayer?: boolean // true = 服务端代付 gas(仅 transaction 模式)
permit2Address?: string // Uniswap Permit2 合约地址
splits?: ChargeSplit[] // 分账,最多 10 项
}
}
type ChargeSplit = {
amount: string // 该路收款 base units
recipient: string // 该路收款方 EVM 地址
memo?: string
}
分账#
填 methodDetails.splits 即可:
methodDetails: {
feePayer: true,
splits: [
{ amount: '30', recipient: '0x...', memo: 'partner-a' },
{ amount: '20', recipient: '0x...', memo: 'partner-b' },
],
}
约束:分账总额必须严格小于 amount(主收款方至少留 1 base unit),最多 10 项;客户端要为每路 split 单独签 EIP-3009(已自动放在 payload.authorization.splits[])。校验由 SA API 强制(违反时 70005 / 70006)。
Session - 按量支付#
按量计费场景:开通 escrow 通道 → 高频提交 voucher → 卖家随时主动 settle / close。
注册#
import { privateKeyToAccount } from 'viem/accounts'
const sellerSigner = privateKeyToAccount(process.env.MERCHANT_PK as `0x${string}`)
const mppx = Mppx.create({
methods: [session({ saClient, signer: sellerSigner })],
realm: 'demo.merchant.com',
secretKey: process.env.MPPX_SECRET_KEY!,
})
工厂参数#
type SessionParameters = {
saClient: SaApiClient // 必填
signer: SessionSigner // 必填,settle / close 时签 EIP-712 授权
chainId?: number // 默认 196
escrowContract?: Hex // 默认 0x5E550002e64FaF79B41D89fE8439eEb1be66CE3b
domainName?: string // 默认 "EVM Payment Channel"
domainVersion?: string // 默认 "1"
store?: SessionStore // 默认进程内存 store
minVoucherDelta?: string // 默认 "0",base units
}
/** 卖家签名能力。viem 的 LocalAccount / WalletClient.account 都满足。 */
type SessionSigner = {
signTypedData: <const td extends TypedDataDefinition>(p: td) => Promise<Hex>
}
escrowContract/domainName/domainVersion必须与链上 escrow 合约的 EIP712Domain 完全一致,否则 voucher / settle / close 验签全部失败。
调用#
async function meter(request: Request): Promise<Response> {
const result = await mppx.session({
amount: '100',
currency: '0x...',
recipient: '0x...',
unitType: 'request',
suggestedDeposit: '10000',
methodDetails: {}, // chainId / escrowContract 自动用工厂默认
})(request)
if (result.status === 402) return result.challenge
// open / topUp / close 这三个管理动作 SDK 强制返 204;
// 只有 voucher 动作真正交付资源,下面这行 Response 会被透传。
return result.withReceipt(Response.json({ data: 'metered content' }))
}
每次请求按 payload.action 走不同路径:
action | 行为 |
|---|---|
open | 校验 payee → 调 SA session/open 上链 → 写本地 store |
voucher | 本地 EIP-712 验签 → 提升最高 voucher → 原子扣费(不调 SA API,纯本地) |
topUp | 调 SA session/topUp → 累加本地 deposit |
close | 卖家签 CloseAuth → 调 SA session/close → 删本地 store |
调用选项#
type SessionOptions = {
amount: string // 单价,base units
currency: string // ERC-20 EVM 地址
recipient: string // 收款方 EVM 地址
description?: string
externalId?: string
unitType?: string // "request" | "byte" | "llm_token" | ...
suggestedDeposit?: string // 建议初次开通存入金额,base units
methodDetails: {
chainId?: number // 工厂默认兜底
escrowContract?: string // 工厂默认兜底
channelId?: string
minVoucherDelta?: string // 节流:voucher 最小递增量
feePayer?: boolean
splits?: SessionSplit[] // 按 bps 分润
}
}
type SessionSplit = {
recipient: string
bps: number // basis points(1‒9999),sum(bps) < 10000
memo?: string
}
扩展方法:主动 settle / status#
session({...}) 返回的对象在 mppx Method 上额外挂了两个调用:
/** 用本地最高 voucher 提链上结算(不关闭通道)。
* 自动签 SettleAuthorization 并提交。 */
mppx.session.settle(channelId: string): Promise<SessionReceipt>
/** 查询链上通道状态。 */
mppx.session.status(channelId: string): Promise<ChannelStatus>
interface SessionReceipt {
method: string // "evm"
intent: string // "session"
status: string // "success"
timestamp: string // RFC 3339
channelId: string
chainId: number
reference?: string // tx hash(transaction 模式)
deposit: string // 链上 escrow 当前存款总额
}
interface ChannelStatus {
channelId: string
payer: string
payee: string
token: string
deposit: string
settledOnChain: string // 已链上结算金额(settle 后才更新)
sessionStatus: 'OPEN' | 'CLOSING' | 'CLOSED'
remainingBalance: string // = deposit - cumulativeAmount
}
自定义 SessionStore#
默认 memoryStore() 单进程够用,但进程重启即丢所有 channel state。长期 channel / 多实例 / 热重载部署需自实现持久化 store(Redis / Postgres / KV / DynamoDB / etcd 都行)。
interface SessionStore {
get(channelId: string):
Promise<ChannelState | null> | ChannelState | null
set(channelId: string, state: ChannelState):
Promise<void> | void
delete(channelId: string):
Promise<void> | void
/** read-modify-write 整体原子。
* state 不存在时不调 mutator,直接返 null。
* 实现侧需保证 mutator 调用期间无并发写入。 */
update(channelId: string, mutator: ChannelMutator):
Promise<ChannelState | null> | ChannelState | null
}
/** 同步纯函数,对 state 就地修改;抛错回滚不写入。
* 不要在 mutator 内做异步 IO(实现可能多次调用它)。 */
type ChannelMutator = (state: ChannelState) => void
update() 是正确性的关键:进程内可用 per-id mutex;Redis 用 Lua;Postgres 用 SELECT ... FOR UPDATE 事务;DynamoDB / etcd 用 CAS 重试。
ChannelState#
interface ChannelState {
channelId: Hex // 主键 = 链上 channelId
chainId: number
escrowContract: Hex
domainName: string
domainVersion: string
signer: Hex // 期望的 voucher 签名者
deposit: bigint // 链上 escrow 当前存款
spent: bigint // 本地累计已扣
units: number // 计费次数
highestVoucherAmount: bigint // 已接受的最高 voucher 金额
highestVoucher: // 字节值(幂等 + idle close 用)
| { cumulativeAmount: string; signature: Hex }
| null
challengeEcho: ChallengeEcho
createdAt: string // ISO 8601
}
SessionStore/ChannelMutator在 v0.1.0 未通过 subpath 透出。结构类型即可匹配,按上面接口签名实现一份传给session({ store: ... })即可。
EIP-712 工具#
构建和校验 session voucher / settle / close 授权用。链上 escrow 合约的 EIP712Domain 默认值:
DEFAULT_DOMAIN_NAME = 'EVM Payment Channel'
DEFAULT_DOMAIN_VERSION = '1'
verifyVoucher#
校验签名是否出自 expectedSigner(基于 viem verifyTypedData / ecrecover)。
function verifyVoucher(params: {
chainId: number
escrowContract: Hex
channelId: Hex
cumulativeAmount: string | bigint
signature: Hex
expectedSigner: Hex
domainName?: string // 默认 "EVM Payment Channel"
domainVersion?: string // 默认 "1"
}): Promise<boolean>
buildSettleAuth / buildCloseAuth#
不签名,只构造 viem TypedDataDefinition,喂给 signer.signTypedData(...) 拿 65 字节签名。两者参数一致:
function buildSettleAuth(p: AuthMessageParams): TypedDataDefinition
function buildCloseAuth(p: AuthMessageParams): TypedDataDefinition
interface AuthMessageParams {
chainId: number
escrowContract: Hex
channelId: Hex
cumulativeAmount: string | bigint
nonce: string | bigint
deadline: string | bigint
domainName?: string
domainVersion?: string
}
randomU256 / unixDeadline#
/** 256-bit 密码学安全随机数,十进制字符串。 */
function randomU256(): string
/** unix 秒,十进制字符串;默认当前时间 + 1 小时。 */
function unixDeadline(secondsFromNow?: number): string
合约层 nonce 已用集 key = (payee, channelId, nonce)。重复使用会 NonceAlreadyUsed revert;SDK 不维护已用集,只负责生成「大概率没用过」的随机值。
SaApiClient#
OKX SA API HTTP 客户端,charge / session 工厂的底层依赖。用户只需把它实例化后传给工厂,不需要直接调它的方法。
new SaApiClient({
apiKey: string
secretKey: string
passphrase: string
baseUrl?: string // 默认 "https://web3.okx.com"
onError?: (info: SaApiErrorInfo) => void
})
interface SaApiErrorInfo {
method: 'GET' | 'POST'
path: string
requestBody?: string
httpStatus: number
code?: number // SA 业务错误码;解析失败为 undefined
msg?: string
responseBody?: string
}
onError 在 HTTP 非 2xx、JSON 解析失败、或业务码非 0 时触发,try/catch 隔离不影响主流程;用于业务侧打 log / 上报。SDK 内部解包后会按错误码抛出对应 PaymentError 子类(见下一节)。
错误处理#
SDK 抛 mppx 顶层 Errors namespace 下的 PaymentError 子类;mppx 自动将其转为 RFC 9457 ProblemDetails 响应。
import { Errors } from '@okxweb3/mpp'
SA API 错误码 → PaymentError 子类#
| code | 含义 | 抛出的 PaymentError |
|---|---|---|
| 8000 | API 服务内部错误 | VerificationFailedError |
| 70000 | 缺字段或格式错误 | VerificationFailedError |
| 70001 | 链不在支持列表 | VerificationFailedError |
| 70002 | 付款方在黑名单 | VerificationFailedError |
| 70003 | source 缺失 / feePayer 与 hash 模式冲突 / txHash 已用 | VerificationFailedError |
| 70004 | 签名验证失败 | InvalidSignatureError |
| 70005 | 分账总额 ≥ 主金额 | InvalidPayloadError |
| 70006 | 分账数量 > 10 | InvalidPayloadError |
| 70007 | 交易未上链 | VerificationFailedError |
| 70008 | 链上 channel 已关闭 | ChannelClosedError |
| 70009 | challenge 不存在 / 已过期 | InvalidChallengeError |
| 70010 | channelId 不存在 | ChannelNotFoundError |
| 70011 | escrow grace period 配置不达标 | InvalidPayloadError |
| 70012 | cumulativeAmount > deposit | AmountExceedsDepositError |
| 70013 | voucher 递增量 < minVoucherDelta | DeltaTooSmallError |
| 70014 | channel 处于 CLOSING 状态 | ChannelClosedError |
错误码常量:
import { SA_ERROR_CODES, type SaErrorCode } from '@okxweb3/mpp/evm'
SA_ERROR_CODES[70004] // "invalid_signature"
session voucher 余额不足#
voucher action 在本地扣费时,若 highestVoucherAmount - spent < amount 抛 Errors.InsufficientBalanceError,mppx 转 402;channel 不存在抛 Errors.ChannelNotFoundError。