How to Complete API Payment#
Overview#
x402 is an on-chain micropayment protocol based on the HTTP 402 (Payment Required) status code. Users authorize transfers via EIP-3009 off-chain signatures — no need to hold OKB or submit on-chain transactions — to complete paid API calls.
Flow Diagram#
Client Server Chain
| | |
| 1. POST /api/xxx | |
|------------------------------>| |
| | |
| 2. 402 + payment required | |
|<------------------------------| |
| | |
| 3. EIP-3009 off-chain sign | |
| via SDK (no gas needed) | |
| or local script | |
| | |
| 4. POST /api/xxx | |
| + PAYMENT-SIGNATURE | |
|------------------------------>| |
| | 5. Verify sig + deduct |
| |----------------------------->|
| | |
| 6. 200 + response data | |
|<------------------------------| |Signing SDK (Automatic 402 Handling)#
Step 1: Install Node.js#
Make sure Node.js >= 18 is installed:
node -v # should output v18.x.x or higher
If not installed, download from https://nodejs.org/.
Step 2: Create Project and Install Dependencies#
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: Configure Environment Variables#
Create a .env file in your project root:
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⚠️ Never hardcode secrets in your code. Add
.envto.gitignore.
Step 4: Ensure Your Wallet Holds Tokens on X Layer#
Your wallet must hold USDG or USDT on X Layer (chainIndex: 196).
- USDG contract address:
0x4ae46a509f6b1d9056937ba4500cb143933d2dc8 - USDT contract address:
0x779ded0c9e1022225f8e0630b35a9b54be713736 - You can withdraw USDT from the OKX exchange to the X Layer network to fund your wallet.
Step 5: Receive x402 Payment Info and Sign via SDK#
When a paid Market API endpoint triggers x402, use the signing SDK to handle it automatically:
1. Create tsconfig.json in your project root:
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"ignoreDeprecations": "6.0"
}
}
2. Save the following as app.ts and run npx ts-node --transpileOnly app.ts:
policies and paymentRequirementsSelector, to specify payment using USDT or USDG. If these methods are not used, the first token in the returned result will be used for payment by default.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 signing
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. Read private key and create wallet account
const pk = process.env.EVM_PRIVATE_KEY;
if (!pk) {
console.error("Error: EVM_PRIVATE_KEY not found, please configure it in .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(`Wallet address: ${account.address}`);
const USDG_XLAYER = "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8";
const USDT_XLAYER = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
// Currently supports paying with either USDG or USDT
// 2. Wrap fetch with SDK to automatically handle 402 payments
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{
network: "eip155:196", // X Layer
client: new ExactEvmScheme(signer),
},
],
// Filter to keep only the specified token
policies: [
(_v, reqs) =>
reqs.filter(
(r) =>
r.network === "eip155:196" &&
// To switch payment token, just change the variable below
r.asset.toLowerCase() === USDG_XLAYER.toLowerCase(),
),
],
// Select one from multiple candidates (e.g., choose the lowest amount)
paymentRequirementsSelector: (_v, reqs) =>
reqs.reduce((a, b) => (BigInt(a.amount) <= BigInt(b.amount) ? a : b)),
});
// 3. Build request and call — automatically signs and retries on 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. Handle response
const data = await response.json();
console.log("Response:", JSON.stringify(data, null, 2));
}
main().catch((err) => {
console.error("Failed:", err);
process.exit(1);
});
3. Sample Output
Success
Wallet address: 0x63294Ef9934d1482Ef5AeF57F225C28ae1B53cc5
Response: {
"code": "0",
"data": [
{
"chainIndex": "501",
"price": "133.71500000",
"time": "1776761078382",
"tokenContractAddress": "So11111111111111111111111111111111111111112"
}
],
"msg": ""
}Failure
{
"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"
}
}
]
}
Manual Signing script#
Step 1: Install Node.js#
Make sure Node.js >= 18 is installed:
node -v # should output v18.x.x or higher
If not installed, download from https://nodejs.org/.
Step 2: Create Project and Install Dependencies#
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: Configure Environment Variables#
Create a .env file in your project root:
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⚠️ Never hardcode secrets in your code. Add
.envto.gitignore.
Create tsconfig.json:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"types": ["node"],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"ignoreDeprecations": "6.0"
}
}
Step 4: Ensure Your Wallet Holds Tokens on X Layer#
Your wallet must hold USDT or USDG on X Layer (chainIndex: 196).
- USDG contract address:
0x4ae46a509f6b1d9056937ba4500cb143933d2dc8 - USDT contract address:
0x779ded0c9e1022225f8e0630b35a9b54be713736
You can use following method to fund your wallet:
- Withdraw USDG/USDT from the OKX exchange to the X Layer network
- Swap to USDG/USDT on-chain via OKX DEX
- Bridge and swap to USDG/USDT on X Layer via OKX Bridge Swap
Step 5: Receive x402 Payment Info and Sign#
When a paid Market API endpoint triggers x402, you will receive the following response:
{
"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"
}
}
]
}
Save the following code as app.ts, run npx ts-node app.ts, obtain the PAYMENT-SIGNATURE, include it in the request header, and retry the API call.
/**
* EIP-3009 TransferWithAuthorization Signing Script
*
* EIP-3009 allows users to sign an "authorized transfer" off-chain.
* After receiving the signature, the server can call the contract method
* transferWithAuthorization() to complete the transfer,
* so the user does not need to hold ETH or send an on-chain transaction.
*
* ── Private Key Configuration ────────────────────────────────────────────────
*
* This script reads the payer's private key from the environment variable EVM_PRIVATE_KEY.
* It supports the following methods:
*
* Method 1: .env file (recommended for development)
* 1. Create a .env file in the same directory:
* EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
* 2. Add .env to .gitignore to avoid committing the private key
* 3. Run: npx ts-node eip3009_sign_only.ts
*
* Method 2: Command-line environment variable (temporary use)
* EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE npx ts-node eip3009_sign_only.ts
*
* Method 3: System environment variable (persistent)
* Add to ~/.zshrc or ~/.bashrc:
* export EVM_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
* Then run source ~/.zshrc
*
* ⚠️ Security Notice:
* - Never hardcode private keys in source code
* - Never commit private keys to Git repositories
* - Use KMS or HSM in production environments
*
* ── Dependencies ─────────────────────────────────────────────────────────────
*
* npm install viem
*/
import "dotenv/config";
import { privateKeyToAccount, signTypedData } from "viem/accounts";
import { randomBytes } from "crypto";
// ── Input Types ──────────────────────────────────────────────────────────────
interface SignParams {
privateKey: string;
network: string;
amount: string;
payTo: string;
asset: string;
maxTimeoutSecs?: number;
domainName: string;
domainVersion: string;
}
// ── Output Types ─────────────────────────────────────────────────────────────
interface SignResult {
signature: string;
authorization: {
from: string;
to: string;
value: string;
validAfter: string;
validBefore: string;
nonce: string;
};
}
// ── EIP-712 Types ────────────────────────────────────────────────────────────
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;
// ── Utility ──────────────────────────────────────────────────────────────────
function parseChainIndex(network: string): number {
const match = network.match(/^eip155:(\d+)$/);
if (!match) {
throw new Error(`Invalid network format: "${network}", expected "eip155:<chainIndex>"`);
}
return parseInt(match[1], 10);
}
// ── Core Signing ─────────────────────────────────────────────────────────────
export async function eip3009Sign(params: SignParams): Promise<SignResult> {
const {
privateKey,
network,
amount,
payTo,
asset,
maxTimeoutSecs = 300,
domainName,
domainVersion,
} = params;
const pk = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
const account = privateKeyToAccount(pk);
const from = account.address;
const chainIndex = parseChainIndex(network);
const validBefore = BigInt(Math.floor(Date.now() / 1000) + maxTimeoutSecs);
const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
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,
},
};
}
Sample Output#
Success
Wallet address: 0x63294Ef9934d1482Ef5AeF57F225C28ae1B53cc5
Response: {
"code": "0",
"data": [
{
"chainIndex": "501",
"price": "133.71500000",
"time": "1776761078382",
"tokenContractAddress": "So11111111111111111111111111111111111111112"
}
],
"msg": ""
}Failure
{
"x402Version": 2,
"error": "invalid signature, nonce_used",
"resource": {
"url": "https://web3.okx.com/api/v6/dex/market/price-info",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"extra": {
"transferMethod": "eip3009",
"name": "Global Dollar",
"symbol": "USDG",
"version": "1"
}
},
{
"scheme": "exact",
"network": "eip155:196",
"amount": "500",
"payTo": "0x0dedc3c5e15bee45166924ea5b02f54a35b1f9c6",
"maxTimeoutSeconds": 86400,
"asset": "0x779ded0c9e1022225f8e0630b35a9b54be713736",
"extra": {
"transferMethod": "eip3009",
"name": "USD₮0",
"symbol": "USD₮0",
"version": "1"
}
}
]
}