SDK 参考

Node SDK 参考#

Node SDK 参考(适用于 exactaggr_deferred#

#

描述
@okxweb3/x402-core核心:客户端、服务端、facilitator、类型
@okxweb3/x402-evmEVM 机制:exact、aggr_deferred
@okxweb3/x402-expressExpress 中间件(卖方)
@okxweb3/x402-nextNext.js 中间件(卖方)
@okxweb3/x402-honoHono 中间件(卖方)
@okxweb3/x402-fastifyFastify 中间件(卖方)
@okxweb3/x402-fetchFetch 包装器(买方)
@okxweb3/x402-axiosAxios 包装器(买方)
@okxweb3/x402-mcpMCP 集成
@okxweb3/x402-paywall浏览器付费墙 UI
@okxweb3/x402-extensions协议扩展

核心类型#

Network#

typescript
type Network = `${string}:${string}`;
// CAIP-2 format, e.g., "eip155:196", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"

Money / Price / AssetAmount#

typescript
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#

typescript
interface ResourceInfo {
  url: string;              // Resource URL path
  description?: string;     // Human-readable description
  mimeType?: string;        // Response content type (e.g., "application/json")
}

PaymentRequirements#

描述卖方接受的支付方式。

typescript
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 字段:

SchemeExtra 字段Type描述
exact (EIP-3009)extra.eip712.namestringEIP-712 域名称(例如 "USD Coin")
exact (EIP-3009)extra.eip712.versionstringEIP-712 域版本(例如 "2")

PaymentRequired#

发送给客户端的 HTTP 402 响应体。

typescript
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#

客户端在重试请求中提交的签名支付。

typescript
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:

typescript
{
  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:

typescript
{
  signature: `0x${string}`;     // Session key signature
  authorization: { /* same as EIP-3009 */ };
  // acceptedExtraOverrides includes sessionCert
}

VerifyResponse#

typescript
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#

typescript
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#

typescript
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)#

构造函数#

typescript
import { x402ResourceServer } from "@okxweb3/x402-core/server";

const server = new x402ResourceServer(facilitatorClients?);
// facilitatorClients: FacilitatorClient | FacilitatorClient[]

register(network, server)#

注册服务端 scheme。可链式调用。

typescript
server
  .register("eip155:84532", new ExactEvmScheme())
  .register("eip155:196", new AggrDeferredEvmScheme());

registerExtension(extension)#

typescript
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。在启动时调用一次。

typescript
await server.initialize();

buildPaymentRequirements(config) → PaymentRequirements[]#

typescript
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 参数。

typescript
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#

typescript
const result = await server.verifyPayment(paymentPayload, requirements);
// result.isValid: boolean

settlePayment(payload, requirements, ...) → SettleResponse#

typescript
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 }
typescript
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 特定逻辑。

构造函数#

typescript
import { x402HTTPResourceServer } from "@okxweb3/x402-core/http";

const httpServer = new x402HTTPResourceServer(resourceServer, routes);

RoutesConfig#

typescript
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)#

typescript
type OnSettlementTimeoutHook = (txHash: string, network: string) => Promise<{ confirmed: boolean }>;

httpServer.onSettlementTimeout(async (txHash, network) => {
  // Custom recovery logic
  return { confirmed: false };
});

onProtectedRequest(hook)#

typescript
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)#

typescript
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: "..." });
});
ParameterTypeDefault描述
routesRoutesConfig必填路由到支付配置的映射
serverx402ResourceServer必填预配置的资源服务器
paywallConfigPaywallConfigundefined浏览器付费墙设置
paywallPaywallProviderundefined自定义付费墙渲染器
syncFacilitatorOnStartbooleantrue在首次请求时获取支持的 kinds

Next.js (@okxweb3/x402-next)#

typescript
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)#

typescript
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-hono";

app.use("/*", paymentMiddleware(routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?));

Fastify (@okxweb3/x402-fastify)#

typescript
import { paymentMiddleware, paymentMiddlewareFromConfig, paymentMiddlewareFromHTTPServer } from "@okxweb3/x402-fastify";

// NOTE: Fastify registers hooks directly, returns void
paymentMiddleware(app, routes, server, paywallConfig?, paywall?, syncFacilitatorOnStart?);

EVM 机制类型#

ExactEvmScheme(服务端)#

typescript
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(服务端)#

typescript
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-axiosAxiosInstance已有 Axios 代码库;需要使用拦截器 / 实例配置
@okxweb3/x402-fetchglobalThis.fetch基于 fetch 的运行时(浏览器、Edge、Node 18+)

两者的 API 形态一致:wrapXxxWithPayment(client_or_fetch, x402Client)wrapXxxWithPaymentFromConfig(client_or_fetch, config)

Axios — @okxweb3/x402-axios#

bash
npm install @okxweb3/x402-axios @okxweb3/x402-evm @okxweb3/x402-core axios
typescript
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#

bash
npm install @okxweb3/x402-fetch @okxweb3/x402-evm @okxweb3/x402-core
typescript
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。

typescript
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 解码:

typescript
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#

字段类型描述
schemesSchemeRegistration[]必填。每一项把 network(如 "eip155:196""eip155:*")与 scheme 客户端(如 new ExactEvmScheme(signer))配对。
policiesPaymentPolicy[]可选。见下方 Policies,按顺序对 accepts 进行过滤 / 转换。
paymentRequirementsSelectorSelectPaymentRequirements可选。从过滤后的列表中挑选最终一项。默认 (version, accepts) => accepts[0]

选择流水线(Selection Pipeline)#

收到 402 后,客户端按三步决定签哪一项:

  1. 按已注册的 scheme 过滤 —— 只保留 network + scheme 都已通过 register() 注册的 accepts
  2. 按顺序应用 policies —— 每个 PaymentPolicy 进一步过滤 / 转换列表。
  3. selector 选择 —— 从过滤后的列表中选出最终要签名的那一项。

第 1 或第 2 步留空数组时客户端直接抛错——不会发起签名。

Policies — PaymentPolicy#

typescript
type PaymentPolicy = (
  x402Version: number,
  paymentRequirements: PaymentRequirements[],
) => PaymentRequirements[];

policy 是个纯函数:拿当前 accepts,返回过滤后的子集(或转换后的副本)。常用场景:金额上限、网络白名单、scheme 偏好等。

typescript
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#

typescript
type SelectPaymentRequirements = (
  x402Version: number,
  paymentRequirements: PaymentRequirements[],
) => PaymentRequirements;

selector 在 policies 之后运行。当过滤后仍剩多项需要明确的挑选逻辑(如选最便宜)时使用:

typescript
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 形式注册:

typescript
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
onAfterPaymentCreationscheme 返回签名后的 payload 后void(仅观察,不可改)
onPaymentCreationFailurescheme 签名时抛错返回 void 继续抛错 · 返回 { recovered: true, payload } 用替代 payload 恢复

同一阶段内的钩子按注册顺序执行。

客户端扩展 — registerExtension#

PaymentRequired 响应里带 extensions 字段、需要 scheme 相关的 payload 增强(如 Gas 代付的 permit 签名)时使用。enrichPaymentPayload 只会在 paymentRequired.extensions 中包含同名 key 时被触发。

typescript
client.registerExtension({
  key: "eip2612GasSponsoring",
  async enrichPaymentPayload(payload, paymentRequired) {
    // 签 EIP-2612 permit,挂在 payload.extensions 上
    return { ...payload, extensions: { ...payload.extensions, /* ... */ } };
  },
});

Node SDK 参考(适用于 chargesession#

安装与导入#

bash
npm install @okxweb3/mpp viem

@okxweb3/mpp 自带上游 mppx 全部命名空间的透传,应用代码通常只需 import 这一个包。viem 用于 session 的 EIP-712 签名(SessionSigner),charge 不需要。

typescript
// 顶层: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 - 单次支付#

注册#

typescript
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!,
})

调用#

typescript
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' }))
}

调用选项#

typescript
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 即可:

typescript
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。

注册#

typescript
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!,
})

工厂参数#

typescript
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 验签全部失败。

调用#

typescript
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

调用选项#

typescript
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 上额外挂了两个调用:

typescript
/** 用本地最高 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 都行)。

typescript
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#

typescript
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 默认值:

typescript
DEFAULT_DOMAIN_NAME    = 'EVM Payment Channel'
DEFAULT_DOMAIN_VERSION = '1'

verifyVoucher#

校验签名是否出自 expectedSigner(基于 viem verifyTypedData / ecrecover)。

typescript
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 字节签名。两者参数一致:

typescript
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#

typescript
/** 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 工厂的底层依赖。用户只需把它实例化后传给工厂,不需要直接调它的方法。

typescript
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 响应。

typescript
import { Errors } from '@okxweb3/mpp'

SA API 错误码 → PaymentError 子类#

code含义抛出的 PaymentError
8000API 服务内部错误VerificationFailedError
70000缺字段或格式错误VerificationFailedError
70001链不在支持列表VerificationFailedError
70002付款方在黑名单VerificationFailedError
70003source 缺失 / feePayer 与 hash 模式冲突 / txHash 已用VerificationFailedError
70004签名验证失败InvalidSignatureError
70005分账总额 ≥ 主金额InvalidPayloadError
70006分账数量 > 10InvalidPayloadError
70007交易未上链VerificationFailedError
70008链上 channel 已关闭ChannelClosedError
70009challenge 不存在 / 已过期InvalidChallengeError
70010channelId 不存在ChannelNotFoundError
70011escrow grace period 配置不达标InvalidPayloadError
70012cumulativeAmount > depositAmountExceedsDepositError
70013voucher 递增量 < minVoucherDeltaDeltaTooSmallError
70014channel 处于 CLOSING 状态ChannelClosedError

错误码常量:

typescript
import { SA_ERROR_CODES, type SaErrorCode } from '@okxweb3/mpp/evm'

SA_ERROR_CODES[70004]   // "invalid_signature"

session voucher 余额不足#

voucher action 在本地扣费时,若 highestVoucherAmount - spent < amountErrors.InsufficientBalanceError,mppx 转 402;channel 不存在抛 Errors.ChannelNotFoundError