如何完成调用支付#
概述#
x402 是基于 HTTP 402(Payment Required)状态码的链上微支付协议。用户通过 EIP-3009 链下签名授权转账,无需持有 OKB、无需主动发链上交易,即可完成 API 付费调用。
流程图#
Client Server Chain
| | |
| 1. POST /api/xxx | |
|------------------------------>| |
| | |
| 2. 402 + 支付要求 | |
|<------------------------------| |
| | |
| 3. 使用 SDK 自动完成 EIP-3009 | |
| 链下签名(无需 gas) | |
| 使用脚本,本地签名 | |
| | |
| 4. POST /api/xxx | |
| + PAYMENT-SIGNATURE | |
|------------------------------>| |
| | 5. 验证签名 + 扣款 |
| |----------------------------->|
| | |
| 6. 200 + 业务数据 | |
|<------------------------------| |签名 SDK(自动处理 402)#
Step 1:安装 Node.js#
确保已安装 Node.js >= 18:
bash
node -v # 应输出 v18.x.x 或更高
如未安装,前往 https://nodejs.org/ 下载。
Step 2:创建项目并安装依赖#
bash
mkdir x402-demo && cd x402-demo
npm init -y
npm install --save-dev @types/node
npm install viem @okxweb3/x402-fetch @okxweb3/x402-evm dotenv ts-node typescript
Step 3:配置环境变量#
在项目根目录创建 .env 文件:
EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
OKX_ACCESS_KEY=YOUR_OKX_ACCESS_KEY
OKX_SECRET_KEY=YOUR_OKX_SECRET_KEY
OKX_PASSPHRASE=YOUR_OKX_PASSPHRASE⚠️ 切勿将密钥硬编码到代码中,将
.env加入.gitignore。
Step 4:确保钱包持有 X Layer 上的 U#
钱包需要在 X Layer(chainIndex: 196) 上持有 USDG 或者 USDT。
- USDG 合约地址:
0x4ae46a509f6b1d9056937ba4500cb143933d2dc8 - USDT 合约地址:
0x779ded0c9e1022225f8e0630b35a9b54be713736 - 可通过 OKX 交易所提现 USDT 到 X Layer 网络充值
Step 5:接收 x402 付款信息并使用 SDK 签名#
当任意行情 API 付费接口触发 x402 付款信息时,可使用签名 SDK 完成自动签名,详细步骤如下:
json
{
"x402Version": 2,
"resource": {
"url": "https://web3.okx.com/api/v6/dex/market/xxx",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"extra": {
"version": "1",
"transferMethod": "eip3009",
"name": "Global Dollar",
"symbol": "USDG"
}
},
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x779ded0c9e1022225f8e0630b35a9b54be713736",
"extra": {
"version": "1",
"transferMethod": "eip3009",
"name": "USD₮0",
"symbol": "USD₮0"
}
}
]
}
1. 在项目根目录创建 tsconfig.json:
json
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"ignoreDeprecations": "6.0"
}
}
2. 将以下代码保存为 app.ts,运行 npx ts-node --transpileOnly app.ts 即可:
提示
SDK 里提供了两个方法
policies 和 paymentRequiremntSelector 用于指定 USDT 或者 USDG 支付,如果不使用该方法将默认使用返回结果里的第一个币种进行支付。typescript
import "dotenv/config";
import {createHmac} from "crypto";
import {wrapFetchWithPaymentFromConfig} from "@okxweb3/x402-fetch";
import {ExactEvmScheme, toClientEvmSigner} from "@okxweb3/x402-evm";
import {privateKeyToAccount} from "viem/accounts";
// OKX API 签名
function createOkxHeaders(method: string, path: string, body: string) {
const timestamp = new Date().toISOString();
const sign = createHmac("sha256", process.env.OKX_SECRET_KEY!)
.update(timestamp + method + path + body)
.digest("base64");
return {
"OK-ACCESS-KEY": process.env.OKX_ACCESS_KEY!,
"OK-ACCESS-SIGN": sign,
"OK-ACCESS-TIMESTAMP": timestamp,
"OK-ACCESS-PASSPHRASE": process.env.OKX_PASSPHRASE!,
};
}
async function main() {
// 1. 读取私钥,创建钱包账户
const pk = process.env.EVM_PRIVATE_KEY;
if (!pk) {
console.error("错误: 未找到 EVM_PRIVATE_KEY,请在 .env 中配置");
process.exit(1);
}
const privateKey = (pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`;
const account = privateKeyToAccount(privateKey);
const signer = toClientEvmSigner(account);
console.log(`钱包地址: ${account.address}`);
const USDG_XLAYER = "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8";
const USDT_XLAYER = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
// 当前支持 USDG 和 USDT 任意一个代币支付
// 2. 用 SDK 包装 fetch,自动处理 402 付费
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{
network: "eip155:196", // X Layer
client: new ExactEvmScheme(signer),
},
],
// 只保留指定币种
policies: [
(_v, reqs) =>
reqs.filter(
(r) =>
r.network === "eip155:196" &&
// 如需替换支付币种,直接变更下方的变量名即可
r.asset.toLowerCase() === USDG_XLAYER.toLowerCase(),
),
],
// 多个匹配项时再做最终选择(比如选 amount 最小的)
paymentRequirementsSelector: (_v, reqs) =>
reqs.reduce((a, b) => (BigInt(a.amount) <= BigInt(b.amount) ? a : b)),
});
// 3. 构建请求并调用 — 遇到 402 会自动签名付费并重试
const url = "https://web3.okx.com/api/v6/dex/market/price-info";
const body = JSON.stringify([
{
chainIndex: 501,
tokenContractAddress: "So11111111111111111111111111111111111111112",
},
]);
const response = await fetchWithPayment(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...createOkxHeaders("POST", new URL(url).pathname, body),
},
body,
});
// 4. 处理响应
const data = await response.json();
console.log("响应:", JSON.stringify(data, null, 2));
}
main().catch((err) => {
console.error("失败:", err);
process.exit(1);
});
3. 运行输出示例
成功示例
钱包地址: 0x63294Ef9934d1482Ef5AeF57F225C28ae1B53cc5
响应: {
"code": "0",
"data": [
{
"chainIndex": "501",
"price": "133.71500000",
"time": "1776761078382",
"tokenContractAddress": "So11111111111111111111111111111111111111112"
}
],
"msg": ""
}错误示例
{
"x402Version": 2,
"error": "invalid payment header",
"resource": {
"url": "https://web3.okx.com/api/v6/dex/market/token/search",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:196",
"amount": "100",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"extra": {
"version": "1",
"symbol": "USDG",
"name": "Global Dollar",
"transferMethod": "eip3009"
}
},
{
"scheme": "exact",
"network": "eip155:196",
"amount": "100",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x779ded0c9e1022225f8e0630b35a9b54be713736",
"extra": {
"version": "1",
"symbol": "USD₮0",
"name": "USD₮0",
"transferMethod": "eip3009"
}
}
]
}手动签名#
Step 1:安装 Node.js#
确保已安装 Node.js >= 18:
bash
node -v # 应输出 v18.x.x 或更高
如未安装,前往 https://nodejs.org/ 下载。
Step 2:创建项目并安装依赖#
bash
mkdir x402-demo && cd x402-demo
npm init -y
npm install --save-dev @types/node
npm install viem @okxweb3/x402-fetch @okxweb3/x402-evm dotenv ts-node typescript
Step 3:配置环境变量#
在项目根目录创建 .env 文件:
EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
OKX_ACCESS_KEY=YOUR_OKX_ACCESS_KEY
OKX_SECRET_KEY=YOUR_OKX_SECRET_KEY
OKX_PASSPHRASE=YOUR_OKX_PASSPHRASE⚠️ 切勿将密钥硬编码到代码中,将
.env加入.gitignore。
创建 tsconfig.json:
json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"types": ["node"],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"ignoreDeprecations": "6.0"
}
}
Step 4:确保钱包持有 X Layer 上的 U#
钱包需要在 X Layer(chainIndex: 196) 上持有 USDT 或者 USDG。
- USDG 合约地址:
0x4ae46a509f6b1d9056937ba4500cb143933d2dc8 - USDT 合约地址:
0x779ded0c9e1022225f8e0630b35a9b54be713736
你可以通过以下几种方式给你的钱包充值 USDG/ USDT 资产
- 从 OKX 交易所提取 USDG/USDT 到 X Layer 网络
- 通过 OKX DEX 在链上将其兑换为 USDG/USDT
- 通过 OKX Bridge Swap 跨链兑换出 X layer 上的 USDG/USDT
Step 5:接收 x402 付款信息并签名#
当任意行情 API 付费接口触发 x402 付款信息时,你将收到如下返回内容:
json
{
"x402Version": 2,
"resource": {
"url": "https://web3.okx.com/api/v6/dex/market/xxx",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"extra": {
"version": "1",
"transferMethod": "eip3009",
"name": "Global Dollar",
"symbol": "USDG"
}
},
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x779ded0c9e1022225f8e0630b35a9b54be713736",
"extra": {
"version": "1",
"transferMethod": "eip3009",
"name": "USD₮0",
"symbol": "USD₮0"
}
}
]
}
将以下代码保存为 app.ts,运行 npx ts-node app.ts ,得到 PAYMENT-SIGNATURE 放在 header 请求头,重新请求一次 API
typescript
/**
* EIP-3009 TransferWithAuthorization 签名脚本
*
* EIP-3009 允许用户对一笔"授权转账"进行链下签名,
* 服务端收到签名后可调用合约 transferWithAuthorization() 完成转账,
* 用户无需持有 ETH 也无需主动发链上交易。
*
* ── 私钥配置说明 ──────────────────────────────────────────────────────────────
*
* 本脚本通过环境变量 EVM_PRIVATE_KEY 读取付款方私钥,支持以下两种方式:
*
* 方式一:.env 文件(推荐开发环境)
* 1. 在脚本同目录创建 .env 文件:
* EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
* 2. 将 .env 加入 .gitignore,避免私钥提交到代码仓库
* 3. 直接运行:npx ts-node eip3009_sign_only.ts
*
* 方式二:命令行环境变量(临时使用)
* EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE npx ts-node eip3009_sign_only.ts
*
* 方式三:系统环境变量(持久生效)
* 在 ~/.zshrc 或 ~/.bashrc 中添加:
* export EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
* 然后执行 source ~/.zshrc 使其生效
*
* ⚠️ 安全提示:
* - 切勿将私钥硬编码到代码中
* - 切勿将含有私钥的文件提交到 Git 仓库
* - 生产环境请使用 KMS(密钥管理服务)或 HSM(硬件安全模块)管理私钥
*
* ── 依赖安装 ──────────────────────────────────────────────────────────────────
*
* npm install viem
*/
import "dotenv/config";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import { randomBytes } from "crypto";
// ── 入参类型 ──────────────────────────────────────────────────────────────────
interface SignParams {
privateKey: string; // 付款方私钥(hex,带或不带 0x 均可)
network: string; // 链标识,格式 "eip155:<chainIndex>",如 "eip155:196"
amount: string; // 支付金额(token 原子单位)
payTo: string; // 收款方地址
asset: string; // token 合约地址
maxTimeoutSecs?: number; // 签名有效期(秒),默认 300
domainName: string; // EIP-712 domain name(从合约 name() 读取)
domainVersion: string; // EIP-712 domain version(从合约 version() 或 DOMAIN_SEPARATOR 反推)
}
// ── 输出类型 ──────────────────────────────────────────────────────────────────
interface SignResult {
signature: string; // EIP-712 签名(0x...,65 字节)
authorization: {
from: string; // 付款方地址(由私钥推导)
to: string; // 收款方地址
value: string; // 金额(原子单位)
validAfter: string; // 生效时间("0" 表示立即生效)
validBefore: string; // 过期时间(Unix 时间戳)
nonce: string; // 随机数(防重放,0x...)
};
}
// ── EIP-712 类型定义(EIP-3009 标准) ────────────────────────────────────────
const TRANSFER_WITH_AUTHORIZATION_TYPE = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
} as const;
// ── 工具函数 ──────────────────────────────────────────────────────────────────
/** 解析 "eip155:196" → 196 */
function parseChainIndex(network: string): number {
const match = network.match(/^eip155:(\d+)$/);
if (!match) {
throw new Error(`网络格式错误: "${network}",应为 "eip155:<chainIndex>"`);
}
return parseInt(match[1], 10);
}
// ── 核心签名函数 ──────────────────────────────────────────────────────────────
export async function eip3009Sign(params: SignParams): Promise<SignResult> {
const {
privateKey,
network,
amount,
payTo,
asset,
maxTimeoutSecs = 300,
domainName,
domainVersion,
} = params;
// 1. 解析私钥,推导 from 地址(无需手动传入)
const pk = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
const account = privateKeyToAccount(pk);
const from = account.address;
// 2. 解析 network → chainIndex
const chainIndex = parseChainIndex(network);
// 3. 计算 validBefore = 当前时间 + 有效期
const validBefore = BigInt(Math.floor(Date.now() / 1000) + maxTimeoutSecs);
// 4. 生成 32 字节随机 nonce(防重放攻击)
const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
// 5. EIP-712 签名
// 签名内容 = \x19\x01 + domainSeparator + hash(TransferWithAuthorization)
const signature = await signTypedData({
privateKey: pk,
domain: {
name: domainName,
version: domainVersion,
chainIndex,
verifyingContract: asset as `0x${string}`,
},
types: TRANSFER_WITH_AUTHORIZATION_TYPE,
primaryType: "TransferWithAuthorization",
message: {
from,
to: payTo as `0x${string}`,
value: BigInt(amount),
validAfter: 0n,
validBefore,
nonce,
},
});
return {
signature,
authorization: {
from,
to: payTo,
value: amount,
validAfter: "0",
validBefore: validBefore.toString(),
nonce,
},
};
}
// ── x402 Payload 类型 ────────────────────────────────────────────────────────
interface X402Resource {
url: string;
mimeType: string;
}
interface X402Accepted {
scheme: string;
network: string;
amount: string;
payTo: string;
maxTimeoutSeconds: number;
asset: string;
extra: Record<string, string>;
}
interface X402Payload {
x402Version: number;
scheme: string;
network: string;
resource: X402Resource;
accepted: X402Accepted;
payload: {
signature: string;
authorization: SignResult["authorization"];
};
}
/** 将 EIP-3009 签名结果包装为 x402 PAYMENT-SIGNATURE header 值(Base64) */
function buildX402Header(signResult: SignResult, resource: X402Resource, accepted: X402Accepted): string {
const payload: X402Payload = {
x402Version: 2,
scheme: "exact",
network: accepted.network,
resource,
accepted,
payload: {
signature: signResult.signature,
authorization: signResult.authorization,
},
};
return Buffer.from(JSON.stringify(payload)).toString("base64");
}
// ── 默认参数(可按需修改) ──────────────────────────────────────────────────
const DEFAULT_SIGN_PARAMS = {
network: "eip155:196", // X Layer mainnet
amount: "500", // 支付金额(token 原子单位)
payTo: "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6", // 收款方地址
asset: "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8", // USDG 合约地址
maxTimeoutSecs: 86400, // 签名有效期 24 小时
domainName: "Global Dollar", // EIP-712 domain name
domainVersion: "1", // EIP-712 domain version
};
const DEFAULT_RESOURCE: X402Resource = {
url: "https://web3.okx.com/api/v6/dex/market/price-info",
mimeType: "application/json",
};
const DEFAULT_ACCEPTED: X402Accepted = {
scheme: "exact",
network: "eip155:196",
amount: "500",
payTo: "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
maxTimeoutSeconds: 86400,
asset: "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
extra: { name: "USDG", version: "1", transferMethod: "eip3009" },
};
// ── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const privateKey = process.env.EVM_PRIVATE_KEY;
if (!privateKey) {
console.error("错误: 未找到 EVM_PRIVATE_KEY 环境变量");
console.error("请在 .env 文件中设置: EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE");
process.exit(1);
}
// 1. EIP-3009 签名
const signResult = await eip3009Sign({
privateKey,
...DEFAULT_SIGN_PARAMS,
});
// 2. 包装为 x402 payload 并 Base64 编码
const paymentSignature = buildX402Header(signResult, DEFAULT_RESOURCE, DEFAULT_ACCEPTED);
// 输出 Base64 编码的 header 值
console.log("\n=== PAYMENT-SIGNATURE header ===\n");
console.log(paymentSignature);
// 输出解码后的 JSON 方便检查
console.log("\n=== 解码后的 payload ===\n");
console.log(JSON.stringify(JSON.parse(Buffer.from(paymentSignature, "base64").toString()), null, 2));
}
main().catch((err) => {
console.error("签名失败:", err);
process.exit(1);
});
运行输出示例#
成功示例
钱包地址: 0x63294Ef9934d1482Ef5AeF57F225C28ae1B53cc5
响应: {
"code": "0",
"data": [
{
"chainIndex": "501",
"price": "133.71500000",
"time": "1776761078382",
"tokenContractAddress": "So11111111111111111111111111111111111111112"
}
],
"msg": ""
}