Node SDK Reference#
Packages#
| Package | Description |
|---|---|
@okxweb3/x402-core | Core: server, facilitator, types |
@okxweb3/x402-evm | EVM mechanisms: exact, aggr_deferred |
@okxweb3/x402-express | Express middleware (seller) |
@okxweb3/x402-next | Next.js middleware (seller) |
@okxweb3/x402-hono | Hono middleware (seller) |
@okxweb3/x402-fastify | Fastify middleware (seller) |
@okxweb3/x402-axios | Axios interceptor (buyer) |
@okxweb3/x402-fetch | fetch wrapper (buyer) |
Core Types#
Network#
type Network = `${string}:${string}`;
// CAIP-2 format, e.g., "eip155:196"
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#
Describes what the seller accepts for payment.
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
};extra fields by scheme:#
| Scheme | Extra Field | Type | Description |
|---|---|---|---|
exact (EIP-3009) | extra.eip712.name | string | EIP-712 domain name (e.g., "USD Coin") |
exact (EIP-3009) | extra.eip712.version | string | EIP-712 domain version (e.g., "2") |
PaymentRequired#
The HTTP 402 response body sent to clients.
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>; // Optional extension data
};
PaymentPayload#
The client's signed payment submitted in the retry request.
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
};
payload fields by scheme:#
For exact (EIP-3009):
{
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
};
}
For aggr_deferred:
{
signature: `0x${string}`; // Session key signature
authorization: { /* same as EIP-3009 */ };
}
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
};
Server API (x402ResourceServer)#
Constructor#
import { x402ResourceServer } from "@okxweb3/x402-core/server";
const server = new x402ResourceServer(facilitatorClients?);
// facilitatorClients: FacilitatorClient | FacilitatorClient[]
register(network, server)#
Register a server-side scheme. Chainable.
server
.register("eip155:196", new ExactEvmScheme())
.register("eip155:196", new AggrDeferredEvmScheme());
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()#
Fetch supported kinds from the facilitator. Call once at startup.
await server.initialize();
**buildPaymentRequirements(config) → PaymentRequirements[]#
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[]#
Dynamic pricing and payTo. Functions receive the context parameter.
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
);
Server Lifecycle Hooks#
| Hook | Context | Can Abort/Recover |
|---|---|---|
onBeforeVerify | { paymentPayload, requirements } | { abort: true, reason, message? } |
onAfterVerify | { paymentPayload, requirements, result } | No |
onVerifyFailure | { paymentPayload, requirements, error } | { recovered: true, result } |
onBeforeSettle | { paymentPayload, requirements } | { abort: true, reason, message? } |
onAfterSettle | { paymentPayload, requirements, result, transportContext? } | No |
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 Resource Server (x402HTTPResourceServer)#
Higher-level wrapper that handles route matching, paywall, and HTTP-specific logic.
Constructor#
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>;
}
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>;
PaywallConfig#
interface PaywallConfig {
appName?: string; // Application name for paywall UI
appLogo?: string; // Logo URL
sessionTokenEndpoint?: string;
currentUrl?: string;
testnet?: boolean; // Show testnet branding
}
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 };
}
});
Middleware Reference#
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)
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 | Description |
|---|---|---|---|
routes | RoutesConfig | required | Route → payment config mapping |
server | x402ResourceServer | required | Pre-configured resource server |
paywallConfig | PaywallConfig | undefined | Browser paywall settings |
paywall | PaywallProvider | undefined | Custom paywall renderer |
syncFacilitatorOnStart | boolean | true | Fetch supported kinds on first request |
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?);
EVM Mechanism Types#
ExactEvmScheme (Server)#
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
const scheme = new ExactEvmScheme(); // No constructor args for server-side
scheme.scheme; // "exact"
// Automatically handles price parsing, EIP-712 domain injection
AggrDeferredEvmScheme (Server)#
import { AggrDeferredEvmScheme } from "@okxweb3/x402-evm/deferred/server";
const scheme = new AggrDeferredEvmScheme();
scheme.scheme; // "aggr_deferred"
// Delegates to ExactEvmScheme for price parsing
Client API (Buyer)#
Buyer-side packages auto-handle 402 Payment Required responses: parse the requirements, sign a payment payload via the configured EVM scheme, retry the request with the PAYMENT header attached.
Two transports are provided — pick the one that matches your HTTP client:
| Package | Wraps | When to use |
|---|---|---|
@okxweb3/x402-axios | AxiosInstance | Existing Axios codebases; access to interceptors / instance config |
@okxweb3/x402-fetch | globalThis.fetch | fetch-based runtimes (browser, edge, Node 18+) |
Both expose the same surface: wrapXxxWithPayment(client_or_fetch, x402Client) and 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";
// Build a viem signer from the buyer's private key
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; use "eip155:*" to match any EVM chain
client: new ExactEvmScheme(signer),
},
],
});
// 402 → sign → retry, all transparent to the caller
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");
Builder pattern with x402Client#
Use the explicit builder when registering multiple schemes / networks, sharing one client across transports, or composing payment selectors.
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 is also re-exported from @okxweb3/x402-fetch, so the same instance works for both transports.
Reading the payment receipt#
After a successful retry, the server returns a PAYMENT-RESPONSE header with the on-chain receipt (txHash, settled amount, etc.). Decode it with decodePaymentResponseHeader:
import { decodePaymentResponseHeader } from "@okxweb3/x402-axios"; // or "@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("Payment receipt:", receipt);
}
x402ClientConfig#
| Field | Type | Description |
|---|---|---|
schemes | SchemeRegistration[] | Required. Each entry pairs a network (e.g. "eip155:196", "eip155:*") with a scheme client (e.g. new ExactEvmScheme(signer)). |
policies | PaymentPolicy[] | Optional. See Policies below. Applied in order to filter / transform accepts before selection. |
paymentRequirementsSelector | SelectPaymentRequirements | Optional. Picks one option from the filtered list. Defaults to (version, accepts) => accepts[0]. |
Selection pipeline#
When a 402 arrives, the client decides which payment option to use in three steps:
- Filter by registered schemes — only
acceptswhosenetwork+schemewere registered viaregister()survive. - Apply policies in registration order — each
PaymentPolicyfurther filters / transforms the list. - Selector picks the single requirement to sign against.
If step 1 or step 2 leaves the list empty, the client throws — payment is never attempted.
Policies — PaymentPolicy#
type PaymentPolicy = (
x402Version: number,
paymentRequirements: PaymentRequirements[],
) => PaymentRequirements[];
A policy is a pure function: take the current accepts, return the filtered subset (or a transformed copy). Use it for spend caps, network whitelists, scheme preferences, etc.
import {
wrapAxiosWithPaymentFromConfig,
type PaymentPolicy,
} from "@okxweb3/x402-axios";
// Reject any option that asks for more than 1 USDT (1_000_000 atomic units, 6 decimals)
const maxAmountPolicy: PaymentPolicy = (_version, reqs) =>
reqs.filter(r => BigInt(r.amount) <= 1_000_000n);
// Only allow X Layer mainnet
const xLayerOnlyPolicy: PaymentPolicy = (_version, reqs) =>
reqs.filter(r => r.network === "eip155:196");
// Prefer "exact" over "aggr_deferred" if both are offered
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 run in array order, so put narrowing filters (caps, whitelists) before preference reorderers.
Custom selector — SelectPaymentRequirements#
type SelectPaymentRequirements = (
x402Version: number,
paymentRequirements: PaymentRequirements[],
) => PaymentRequirements;
Runs after policies. Use it when policies returned multiple equally-valid options and you want explicit picking logic (e.g., cheapest first):
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 exposes three lifecycle hooks for instrumentation, last-mile gating, and recovery. Use the builder form to register them:
import { wrapAxiosWithPayment, x402Client } from "@okxweb3/x402-axios";
import { ExactEvmScheme } from "@okxweb3/x402-evm";
const client = new x402Client()
.register("eip155:196", new ExactEvmScheme(signer))
// 1. Before signing — can abort the payment entirely
.onBeforePaymentCreation(async ({ paymentRequired, selectedRequirements }) => {
const tooExpensive = BigInt(selectedRequirements.amount) > 5_000_000n;
if (tooExpensive) {
return { abort: true, reason: "Amount exceeds buyer policy" };
}
})
// 2. After successful sign — pure observation (logging, metrics)
.onAfterPaymentCreation(async ({ paymentPayload }) => {
console.log("Signed payload nonce:", paymentPayload.payload?.authorization?.nonce);
})
// 3. On signing failure — can recover by returning a manually-built payload
.onPaymentCreationFailure(async ({ error }) => {
console.error("Payment creation failed:", error.message);
// return { recovered: true, payload: fallbackPayload };
});
const api = wrapAxiosWithPayment(axios.create(), client);
| Hook | When | Return semantics |
|---|---|---|
onBeforePaymentCreation | After selection, before scheme signs | void to continue · { abort: true, reason } to cancel and reject |
onAfterPaymentCreation | After scheme returns the signed payload | void only (observation) |
onPaymentCreationFailure | When scheme throws during signing | void to re-throw · { recovered: true, payload } to substitute a payload |
Hooks within the same stage run in registration order.
Client extensions — registerExtension#
Use when a PaymentRequired response carries an extensions field that needs mechanism-specific payload enrichment (e.g., gas-sponsoring permits). The extension's enrichPaymentPayload hook only runs when its key matches a key in paymentRequired.extensions.
client.registerExtension({
key: "eip2612GasSponsoring",
async enrichPaymentPayload(payload, paymentRequired) {
// Sign an EIP-2612 permit and attach to payload.extensions
return { ...payload, extensions: { ...payload.extensions, /* ... */ } };
},
});
Error Classes#
class VerifyError extends Error {
readonly invalidReason?: string;
readonly invalidMessage?: string;
readonly payer?: string;
readonly statusCode: number;
}
class SettleError extends Error {
readonly errorReason?: string;
readonly errorMessage?: string;
readonly payer?: string;
readonly transaction: string;
readonly network: Network;
readonly statusCode: number;
}
class FacilitatorResponseError extends Error {}
class RouteConfigurationError extends Error {
readonly errors: RouteValidationError[];
}
interface RouteValidationError {
routePattern: string;
scheme: string;
network: Network;
reason: "missing_scheme" | "missing_facilitator";
message: string;
}