指南

如何完成调用支付#

概述#

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 里提供了两个方法 policiespaymentRequiremntSelector 用于指定 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": ""
}