Node SDK 参考#
Node SDK 参考(适用于 exact、aggr_deferred、period)#
包#
| 包 | 描述 |
|---|---|
@okxweb3/x402-core | 核心:客户端、服务端、facilitator、类型、订阅支持 |
@okxweb3/x402-evm | EVM 机制:exact、aggr_deferred、period(订阅) |
@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" | "period"
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") |
period(订阅) | extra.plan / extra.amountPerPeriod / extra.periodSec / extra.contracts / extra.facilitator / extra.domain | — | 见"订阅支付"章节 |
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
}
period(订阅) 的 payload:
{
permitSingle: { // Permit2 PermitSingle 授权
details: { token, amount, expiration, nonce };
spender: string; // subscription 合约地址
sigDeadline: string;
};
permitSingleSignature: `0x${string}`;
terms: { // SubscriptionTerms EIP-712 消息(见订阅章节)
payer, merchant, facilitator, token, amountPerPeriod, periodSec, periodMode,
maxPeriods, startAt, initialChargePeriods, initialChargeAmount,
planTier, changeFromSubId, changeEffectiveAt, permitHash, salt, termsDeadline,
};
termsSignature: `0x${string}`;
}
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
};
period scheme 的 SupportedKind.extra 额外携带三个字段:facilitatorAddress / subscriptionContract / permit2Contract(详见"订阅支付"章节)。
服务端 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())
.register("eip155:196", new PermitSubscriptionScheme({ /* ... */ })); // 订阅
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" | "period"
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>;
// ── 订阅(period)专属 ─────────────────────────────
/**
* 订阅特殊操作路由:
* "change" — 切换套餐(升 / 降级;同一 URL 分两阶段)
* "cancel" — 取消订阅
* "cancel-pending-change" — 撤销排队中的降级
* 未设置 → 普通访问路由。
*/
operation?: "change" | "cancel" | "cancel-pending-change";
/**
* 路由级 onBeforeAccess,语义与 httpServer.onBeforeAccess() 相同,
* 仅作用于本 route;在所有 seller 全局 hook 之后执行。
*/
onBeforeAccess?: OnBeforeAccessHook;
}
interface PaymentOption {
scheme: string; // "exact" | "aggr_deferred" | "upto" | "period"
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 };
}
});
onBeforeAccess(hook) — 订阅专属#
seller 级 chain-of-responsibility,verifyAccess 通过后、handler 之前触发。仅对订阅(period scheme)的 access-verified 请求触发。可多次注册,按注册顺序执行;第一个返回 { ok: false } 直接拒绝 → 402。路由级 RouteConfig.onBeforeAccess 在所有全局 hook 之后执行。详见"订阅支付"章节。
httpServer.onBeforeAccess(async (ctx) => {
if (banList.has(ctx.subscription.subId)) return { ok: false, error: "banned" };
return { ok: true };
});
HTTPAdapter.getHeaders?()#
HTTPAdapter 上新增可选方法 getHeaders?(): Record<string, string>(小写 key);官方四个 adapter(express / fastify / hono / next)均已实现。订阅 onBeforeAccess 从 ctx.request.headers 读全部请求头依赖此方法。
中间件参考#
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) — 订阅场景需要挂 onBeforeAccess 时用这个
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?);
四个中间件对订阅场景均支持 payment-presettle 与 access-verified 两种订阅分派(详见"订阅支付"章节)。
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
PermitSubscriptionScheme(订阅,服务端)#
import { PermitSubscriptionScheme } from "@okxweb3/x402-evm/subscription";
const scheme = new PermitSubscriptionScheme({
facilitator, // SubscriptionFacilitatorClient
network: "eip155:196",
store, // SubscriptionStore(与 SubscriptionClient 共享同一实例)
accessProofWindowSec: 300, // AccessProof 时间窗,默认 ±300s
});
scheme.scheme; // "period"
详见"订阅支付"章节的 SubscriptionCapability 接口。
客户端 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, /* ... */ } };
},
});
订阅支付(period scheme)#
period 是 x402 v2 SDK 支持的第三种 scheme(前两种是 exact / aggr_deferred)。买家一次双签(Permit2 PermitSingle + SubscriptionTerms)授权 N 期扣款;商家侧 middleware 自动处理五条链路:创建订阅 / 访问放行 / 切换套餐 / 取消 / 定时扣款。因该 scheme 引入了额外的 Store / Client / hook 与 wire 字段,单列一节展开说明。
顶层入口#
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import { x402HTTPResourceServer, x402ResourceServer } from "@okxweb3/x402-core/server";
import {
InMemoryStore,
SubscriptionClient,
type Subscription, type SubscriptionState, type PendingPlanChange,
type PlanCatalogEntry, type PlanInitialCharge,
type AccessProof, type CancelAuth, type PendingChangeCancelAuth,
type ChargeResult,
type SettleSubscribeResult, type SettleChangeResult,
type SettleCancelResult, type SettleCancelPendingChangeResult,
type SubscriptionCapability, type SubscriptionStore,
type OnBeforeAccessHook, type OnBeforeAccessContext, type OnBeforeAccessResult,
type AccessRouteRequirements,
} from "@okxweb3/x402-core/subscription";
import { PermitSubscriptionScheme } from "@okxweb3/x402-evm/subscription";
import { paymentMiddleware, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-express";
PermitSubscriptionScheme
new PermitSubscriptionScheme({
facilitator: SubscriptionFacilitatorClient, // OKXFacilitatorClient / HTTPFacilitatorClient
network: Network, // "eip155:196"
store: SubscriptionStore, // 与 SubscriptionClient 共享
accessProofWindowSec?: number, // 默认 300(±300s)
});
实现 SubscriptionCapability 全部方法。用 x402ResourceServer.register(network, scheme) 挂载。
SubscriptionClient
seller 侧主动操作的高层封装(定时扣款 / 商家取消 / 从链上同步)。
class SubscriptionClient {
constructor(config: { scheme: SubscriptionCapability; store: SubscriptionStore });
charge(subId: string): Promise<ChargeResult>;
cancelBySeller(subId: string, auth: CancelAuth, reason?: string): Promise<void>;
syncFromChain(subId: string): Promise<Subscription | null>;
getSubscription(subId: string): Promise<Subscription | null>;
}
InMemoryStore / SubscriptionStore
interface SubscriptionStore {
get(subId: string): Promise<Subscription | null>;
put(sub: Subscription): Promise<void>;
delete(subId: string): Promise<void>;
list(): Promise<Subscription[]>;
}
InMemoryStore 仅用于 demo / 单机;生产环境自实现持久化 store(Redis / Postgres / KV 等)。
OKXFacilitatorClient(订阅方法)
除通用 verifyPayment / settlePayment / getSupported,还实现了 SubscriptionFacilitatorClient:
interface SubscriptionFacilitatorClient {
subscribe(payload, requirements): Promise<...>;
changeSubscription(payload, requirements): Promise<...>;
cancelSubscription(subId, auth): Promise<...>;
cancelPendingChange(subId, auth): Promise<...>;
chargeSubscription(subId): Promise<...>;
getSubscription(subId): Promise<...>;
finalizeExpired(subId): Promise<...>;
getCharges(subId): Promise<...>;
getPendingChange(subId): Promise<...>;
}
核心类型#
Subscription
facilitator GET /subscriptions/detail 的本地投影;Store 存的就是这个。
interface Subscription {
subId: string;
payer: string;
merchant: string;
token: string;
amountPerPeriod: string;
periodMode: number; // 0 fixed_seconds / 1 calendar_month
periodSec: number;
billingAnchorAt?: number; // 自然月锚点(Unix s),fixed 模式为 undefined 或 0
maxPeriods: number;
startAt: number;
state: SubscriptionState;
lastChargedPeriod: number;
totalPulled: string; // 已扣总额
planId: string;
planTier: number;
changedToSubId?: string; // state === "changed" 时指向新 sub
pendingPlanChange?: PendingPlanChange;
// 读时刻派生(随墙钟漂移;判断过期请用 elapsedPeriods)
isActive?: boolean;
serviceEnded?: boolean;
currentPeriod?: number;
elapsedPeriods?: number;
nextChargeableAt?: number; // 下一次可扣时间点(Unix s),全部扣完为 null
}
SubscriptionState
type SubscriptionState =
| "pending" // 0 — 已上链,等待激活
| "active" // 1 — 活跃
| "completed" // 2 — maxPeriods 全部扣完
| "canceled" // 3 — 已取消
| "changed" // 4 — 已被切换(changedToSubId 指向新 sub)
| "failed"; // 99 — 上链失败 / 异常
PendingPlanChange
排队中的降级(升级立即生效不产生 pending)。
interface PendingPlanChange {
subId: string; // 当前订阅
newSubId: string; // 降级后新 sub id
effectiveFromPeriod: number; // 从第几期开始切换
state: number; // 0 pending / 1 activated / 2 canceled / 3 expired
}
PlanCatalogEntry / PlanInitialCharge
Seller 侧套餐定义,作为 RouteConfig.accepts 的输入原料。
interface PlanCatalogEntry {
id: string; // 业务 plan id(访问校验按此比对)
tier: number; // 数字越大等级越高
amountPerPeriod: string; // 每期扣款金额(token base units)
periodMode?: 0 | 1; // 0 fixed_seconds / 1 calendar_month
periodSec: number; // 周期长度(秒)
maxPeriods: number; // 最多扣款期数
asset?: string; // ERC-20;不填 → SDK 用 network 默认稳定币
payTo: string;
initialCharge?: PlanInitialCharge;
name?: string;
}
interface PlanInitialCharge {
periodCount: number; // 首扣覆盖多少期
totalAmount: string; // 首扣总额(≤ periodCount × amountPerPeriod)
}
AccessProof
买家在访问 / 切换 / 取消订阅路由时携带的鉴权,EIP-191 personal_sign,±accessProofWindowSec 窗口。
interface AccessProof {
kind: "subscription-id";
subId: string;
payer: string;
timestamp: number;
signature: string;
}
请求头:APP-Access: base64url(JSON.stringify(accessProof))
CancelAuth
EIP-712 取消授权。买家 / 商家均可作为 initiator。
interface CancelAuth {
action: 0; // 固定 0 = cancel_subscription
subId: string;
initiator: 0 | 1; // 0=payer / 1=merchant
nonce: string;
deadline: number;
signature: string;
}
TypeHash:CancelAuth(uint8 action, bytes32 subId, uint8 initiator, bytes32 nonce, uint64 deadline)
PendingChangeCancelAuth
EIP-712 撤销排队中的降级授权。仅 payer 可签。
interface PendingChangeCancelAuth {
subId: string;
newSubId: string; // 必须等于当前 pendingPlanChange.newSubId
nonce: string;
deadline: number;
signature: string;
}
TypeHash:PendingChangeCancelAuth(bytes32 subId, bytes32 newSubId, bytes32 nonce, uint64 deadline)
Verify / Settle 结果#
type VerifyResultOk = { ok: true };
type VerifyResultFail = { ok: false; error: string };
interface VerifyAccessOk { ok: true; subscription: Subscription; }
interface VerifyOwnershipOk { ok: true; subId: string; payer: string; subscription: Subscription; }
interface VerifyChangeOk { ok: true; oldSubId: string; direction: "upgrade" | "downgrade"; }
type SettleResultFail = {
success: false;
error: string;
subId?: string; // pending=true 时 seller 可后续 syncFromChain(subId)
pending?: boolean;
};
interface SettleSubscribeOk { success: true; subId: string; subscription: Subscription; headers: Record<string, string>; }
interface SettleChangeOk { success: true; oldSubId: string; newSubId: string; operationType: "upgrade" | "downgrade"; scheduledFromPeriod?: number; headers: Record<string, string>; }
interface SettleCancelOk { success: true; subId: string; headers: Record<string, string>; }
interface SettleCancelPendingChangeOk { success: true; subId: string; headers: Record<string, string>; }
interface ChargeResult {
success: true;
period: number; // 本次扣款推进到的期数
amount: string; // 本次扣款金额(base units)
txHash?: string;
planChangeTriggered?: boolean; // 排队中的降级此次激活
newSubId?: string; // planChangeTriggered=true 时的新 sub id
}
SubscriptionCapability#
interface SubscriptionCapability {
readonly settlementMode: "pre"; // 中间件采用"先 settle 再 handler"
verifySubscribe(payload, requirements): Promise<VerifyResult>;
settleSubscribe(payload, requirements): Promise<SettleSubscribeResult>;
enrichAcceptsForChange(accepts: PaymentRequirements[], currentSubId: string): Promise<PaymentRequirements[] | null>;
verifyChange(payload, requirements): Promise<VerifyChangeResult>;
settleChange(payload, requirements): Promise<SettleChangeResult>;
verifyCancel(auth: CancelAuth, subId: string): Promise<VerifyResult>;
settleCancel(auth: CancelAuth, subId: string): Promise<SettleCancelResult>;
verifyCancelPendingChange(auth: PendingChangeCancelAuth, subId: string): Promise<VerifyResult>;
settleCancelPendingChange(auth: PendingChangeCancelAuth, subId: string): Promise<SettleCancelPendingChangeResult>;
verifyAccess(proof: AccessProof, route: AccessRouteRequirements): Promise<VerifyAccessResult>;
verifyOwnership(proof: AccessProof): Promise<VerifyOwnershipResult>;
charge(subId: string): Promise<ChargeResult>;
getSubscription(subId: string): Promise<Subscription | null>;
}
OnBeforeAccessContext / Result / Hook#
interface OnBeforeAccessContext {
subscription: Subscription; // 命中订阅的完整快照
request: {
path: string; // 请求 pathname(真实值)
method: string; // HTTP method(真实值)
headers: Record<string, string>; // 全部请求头(小写 key);adapter 未实现 getHeaders 则为 {}
};
route: AccessRouteRequirements; // 本 route 的 plan 声明
}
type OnBeforeAccessResult =
| { ok: true }
| {
ok: false;
error?: string; // 直接进 402 响应体
retryAfter?: number; // 进 Retry-After 响应头
upgradeOffers?: PaymentRequirements[]; // 建议买家改用的 accept 列表
};
type OnBeforeAccessHook = (ctx: OnBeforeAccessContext) => Promise<OnBeforeAccessResult>;
AccessRouteRequirements#
interface AccessRouteRequirements {
acceptedPlanIds?: string[]; // route accepts 里的 plan id 白名单
accepts?: PaymentRequirements[]; // 完整 accepts;extra.plan = { id, tier, name }
// 同时带 extra.amountPerPeriod / extra.periodSec /
// extra.periodMode / extra.maxPeriods
}
undefined 的 acceptedPlanIds 表示"无 plan 限制",任意活跃订阅通过,慎用。
/supported 返回的 extra 字段#
facilitator GET /supported 里每个 kinds[] 的 extra,period scheme 会缓存以下三个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
facilitatorAddress | string | facilitator EOA;签入 Permit2 witness.facilitator,也是链上唯一被信任的 settle 触发者 |
subscriptionContract | string | A2APaySubscription 合约地址(EIP-712 verifyingContract) |
permit2Contract | string | Uniswap Permit2 合约地址 |
任一缺失 → SDK 抛 period supportedKind.extra is missing required fields ...。
PaymentRequirements.extra(订阅 wire format)#
seller 下发 402 时,accepts[].extra 的完整结构(SubscriptionRequirementsExtra):
{
contracts: { subscription: string; permit2: string };
facilitator: string; // 从 /supported.extra.facilitatorAddress 拷贝
amountPerPeriod: string;
periodSec: number;
periodMode?: number; // 0 fixed_seconds / 1 calendar_month
maxPeriods: number;
startAt?: number;
initialCharge?: PlanInitialCharge;
plan: { id: string; tier: number; name?: string };
changeFrom?: ChangeFromExtra; // 只在 operation="change" 的 402 里出现
domain: TypedDataDomain; // EIP-712 域(name / version / chainId / verifyingContract)
}
changeFrom(切换套餐第一阶段 402 中):
{
fromSubId: string;
fromPlanId: string;
fromPlanTier: number;
direction: "upgrade" | "downgrade";
effectiveAt: "immediate" | "period_end";
}
SubscriptionTerms EIP-712 结构#
买家签的第二把签名(第一把是 Permit2 PermitSingle),绑定套餐条款:
SubscriptionTerms(
address payer,
address merchant,
address facilitator,
address token,
uint256 amountPerPeriod,
uint256 periodSec,
uint8 periodMode,
uint256 maxPeriods,
uint256 startAt,
uint256 initialChargePeriods,
uint256 initialChargeAmount,
uint256 planTier,
bytes32 changeFromSubId,
uint8 changeEffectiveAt, // 0=none / 1=immediate / 2=period_end
bytes32 permitHash,
bytes32 salt,
uint64 termsDeadline
)Domain:(name="A2APaySubscription", version="1", chainId, verifyingContract=subscriptionContract)
中间件订阅分支#
四个官方 middleware(express / fastify / hono / next)均支持订阅两条新的 dispatch 分支:
| Result type | 触发场景 | 中间件行为 |
|---|---|---|
payment-presettle | subscribe / change / cancel / cancel-pending-change 路由 | 先 settle(调 facilitator 完成上链),成功后把 settleResult.data.subscription / subId 挂到 req.x402,再进 handler;失败直接 402 |
access-verified | 携带 APP-Access 头访问订阅资源路由 | 无 facilitator / 无链上交互,本地 verifyAccess,通过后挂 req.x402.subscription,进 handler |
seller handler 从 req.x402 读:
app.post("/subscription/cancel", (req, res) => {
const x402 = (req as any).x402;
res.json({ subId: x402.settleResult?.data?.subId });
});
响应头:settle 成功时中间件回写 PAYMENT-RESPONSE(JSON base64url),内容 { subId, txHash, state, ... }。
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 项
resourceUrl?: string // Endpoint URL,用于后台按 URL 维度统计(见下方"resourceUrl 用法")
}
}
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)。
resourceUrl(按 endpoint 维度上报)#
商家想按 endpoint 维度统计 charge 交易量 / 收入时,在 methodDetails 里传 resourceUrl。SDK 会 base64url 编入 challenge.request,透传到 SA API /charge/settle 与 /charge/verifyHash 供后台入库。
async function handler(request: Request): Promise<Response> {
const result = await mppx.charge({
amount: '100',
currency: '0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035',
recipient: '0x742d35Cc6634c0532925a3b844bC9e7595F8fE00',
methodDetails: {
feePayer: true,
resourceUrl: 'https://api.myshop.com/v1/reports', // 按此 URL 打点
},
})(request)
if (result.status === 402) return result.challenge
return result.withReceipt(Response.json({ data: '...' }))
}
处理链路:
Seller SDK (mppx.charge)
└── methodDetails.resourceUrl
↓ base64url 编入 ChargeRequest → PaymentChallenge.request
Buyer 回显 (ChallengeEcho)
↓ POST /charge/settle | /charge/verifyHash
SA API 后端(按 resourceUrl 归集统计)限制:
- Session 模式不支持 —— 单会话内 voucher 高频提交可能跨多 URL,统计会串。
- SA API 后端从
challenge.request里解析resource_url时兼容缺省(空即不落库)。
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
}
Session 不支持
resourceUrl字段:单会话可跨多 URL 提交 voucher,统计会串。按 endpoint 维度上报请使用 charge 模式。
扩展方法:主动 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。
- Node SDK 参考(适用于 exact、aggr_deferred、period)包核心类型NetworkMoney / Price / AssetAmountResourceInfoPaymentRequirementsPaymentRequiredPaymentPayloadVerifyResponseSettleResponseSupportedKind / SupportedResponse服务端 API (x402ResourceServer)构造函数register(network, server)registerExtension(extension)initialize()buildPaymentRequirements(config) → PaymentRequirements[]buildPaymentRequirementsFromOptions(options, context) → PaymentRequirements[]verifyPayment(payload, requirements) → VerifyResponsesettlePayment(payload, requirements, ...) → SettleResponse服务端生命周期钩子HTTP 资源服务器 (x402HTTPResourceServer)构造函数RoutesConfigonSettlementTimeout(hook)onProtectedRequest(hook)onBeforeAccess(hook) — 订阅专属HTTPAdapter.getHeaders?()中间件参考Express (@okxweb3/x402-express)Next.js (@okxweb3/x402-next)Hono (@okxweb3/x402-hono)Fastify (@okxweb3/x402-fastify)EVM 机制类型ExactEvmScheme(服务端)AggrDeferredEvmScheme(服务端)PermitSubscriptionScheme(订阅,服务端)客户端 API(买方)Axios — @okxweb3/x402-axiosFetch — @okxweb3/x402-fetch使用 x402Client 的 builder 模式读取支付回执x402ClientConfig选择流水线(Selection Pipeline)Policies — PaymentPolicy自定义 selector — SelectPaymentRequirements生命周期钩子(Lifecycle Hooks)客户端扩展 — registerExtension订阅支付(period scheme)顶层入口核心类型Verify / Settle 结果SubscriptionCapabilityOnBeforeAccessContext / Result / HookAccessRouteRequirements/supported 返回的 extra 字段PaymentRequirements.extra(订阅 wire format)SubscriptionTerms EIP-712 结构中间件订阅分支Node SDK 参考(适用于 charge、session)安装与导入Charge - 单次支付注册调用调用选项分账resourceUrl(按 endpoint 维度上报)Session - 按量支付注册工厂参数调用调用选项扩展方法:主动 settle / status自定义 SessionStoreChannelStateEIP-712 工具verifyVoucherbuildSettleAuth / buildCloseAuthrandomU256 / unixDeadlineSaApiClient错误处理SA API 错误码 → PaymentError 子类session voucher 余额不足
