One-time Payment#
HTTP Sellers focus on: Business flow → HTTP Seller integration (scheme selection → exact path (incl. Permit2) / charge path / upto path → syncSettle decision) → Advanced (splits, supporting exact + charge simultaneously)
Agent Sellers focus on: Business flow → Agent Seller integration (payment link generation and delivery)
For definitions and the underlying protocol, see Core Concepts · One-time payment. This page focuses on integration.
When it fits#
| Your business | Suitable? |
|---|---|
| Fixed, well-defined amount per call (a report / one inference / one query) | ✅ |
| Price known before the call, no further consumption afterward | ✅ |
| Non-revocable resource (e.g. file download, report generation) | ✅ (recommend syncSettle: true) |
| Per-call cost not knowable upfront, needs settling by actual usage (e.g. LLM inference billed per token) | ✅ (use upto) |
Business flow#
The diagram below is the abstract flow — for HTTP sellers it's "triggered by a client request", for Agent sellers it's "the Agent proactively generates a payment link in the conversation", but the Challenge / Credential message semantics are identical for both. See the seller integration sections below for the concrete transport differences.
KYT (on-chain risk screening) is a compliance check internal to the Broker's Verify phase, not a standalone service.
HTTP Seller integration#
Choosing exact, charge, or upto?#
exact — single recipient, supports both sync and async settlement; collects stablecoins by default, and adding Permit2 extends it to any ERC-20.
charge — besides a single recipient, also supports splits, i.e. paying multiple recipients (≤10) in one payment; sync settlement only (the default and only option).
upto — single recipient, price is a cap, settled by actual usage after the call — suitable when the cost isn't knowable upfront.
| Dimension | exact | charge | upto |
|---|---|---|---|
| Recipients | Single | Single / multiple (≤10) | Single |
| When the amount is fixed | Before the call | Before the call | After the call, by actual (≤ cap) |
| Settlement timing | Sync / async | Sync only (default) | Sync / async |
| Pricing token | EIP-3009 stablecoins (Permit2 → any ERC-20) | EIP-3009 stablecoins | Any ERC-20 (Permit2) |
Not sure which to pick? Use supporting exact + charge simultaneously to mount both and let the buyer choose.
SDK status#
| Scheme | Node.js | Rust | Go | Java | Python |
|---|---|---|---|---|---|
exact | ✅ | ✅ | ✅ | ✅ | ✅ |
charge | ✅ | ✅ | ✅ | Coming soon | ✅ |
upto | ✅ | ✅ | Coming soon | Coming soon | Coming soon |
Permit2 signing for
exactanduptoare currently supported only on Node.js / Rust; other languages are coming soon.
exact path#
Each tab contains the full install command + implementation code. The architecture is composed of 4 components: Facilitator client (with OKX API Key) → Resource Server (registers the scheme) → Routes config (the accepts array) → Middleware mount.
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
import express from "express";
import {
paymentMiddleware,
x402ResourceServer,
} from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
const app = express();
const NETWORK = "eip155:196";
const PAY_TO = process.env.PAY_TO_ADDRESS || "0xYourSellerWallet";
const facilitatorClient = new OKXFacilitatorClient({
apiKey: "OKX_API_KEY",
secretKey: "OKX_SECRET_KEY",
passphrase: "OKX_PASSPHRASE",
});
const resourceServer = new x402ResourceServer(facilitatorClient);
resourceServer.register(NETWORK, new ExactEvmScheme());
app.use(
paymentMiddleware(
{
"GET /api/premium": {
accepts: [
{
scheme: "exact",
network: NETWORK,
payTo: PAY_TO,
price: "$0.10",
syncSettle: true, // Sync settlement: wait for on-chain confirmation
},
],
description: "Premium API",
mimeType: "application/json",
},
},
resourceServer,
),
);
app.get("/api/premium", (_req, res) => {
res.json({ data: "premium content" });
});
app.listen(4000, () => {
console.log("[Seller] listening at http://localhost:4000");
});
Multiple schemes are all declared via the
accepts: [...]array; to addaggr_deferred(batch payment), just add another entry to the array — see Batch Payment.
exact + Permit2: support any ERC-20#
By default, exact uses EIP-3009 signing and can only collect stablecoins that natively support EIP-3009 (USD₮0 / USDG); buyers need no on-chain approve and pay with a single off-chain signature.
To accept any ERC-20 on X Layer, declare one field in the accepts entry — extra: { assetTransferMethod: "permit2" } — and buyers will sign Permit2 instead. The scheme name stays exact, and the route structure and settlement flow (sync / async) are unchanged; the only difference is that the buyer must do a one-time approve for that token (authorizing the Permit2 contract) on first use.
| EIP-3009 (default) | Permit2 | |
|---|---|---|
| Token range | Only stablecoins that natively support EIP-3009 | Any ERC-20 |
| Buyer's first-time cost | Zero (pure signature) | One-time approve to the Permit2 contract |
| Subsequent payments | Pure signature | Pure signature after approve |
package.json:
{
"dependencies": {
"@okxweb3/x402-core": "^0.2",
"@okxweb3/x402-evm": "^0.2",
"@okxweb3/x402-express": "^0.2",
"express": "^4.19"
}
}
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import { paymentMiddleware, x402ResourceServer } from "@okxweb3/x402-express";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
const facilitator = new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const resourceServer = new x402ResourceServer(facilitator)
.register("eip155:196", new ExactEvmScheme());
// The one difference vs plain `exact`:
// extra.assetTransferMethod = "permit2"
// tells the buyer to sign Permit2 instead of EIP-3009. Same scheme name
// ("exact"), same route shape — only the transfer method changes.
const routes = {
"GET /api/premium": {
accepts: {
scheme: "exact",
network: "eip155:196",
payTo: process.env.PAY_TO_ADDRESS!,
price: "$0.05",
extra: { assetTransferMethod: "permit2" },
},
description: "Premium API — paid via Permit2 (any ERC-20 on X Layer)",
mimeType: "application/json",
},
};
const app = express();
app.use(paymentMiddleware(routes, resourceServer));
app.get("/api/premium", (_req: Request, res: Response) => {
res.json({ data: "premium content" });
});
app.listen(4000, async () => {
await resourceServer.initialize();
});
Sync vs. async settlement#
After /settle is called, the Facilitator submits the transaction on-chain. The syncSettle field in the route config controls the settlement behavior:
| Mode | Value | Facilitator behavior | When to use |
|---|---|---|---|
| Sync | true | Submits the transaction and returns txHash after on-chain confirmation | High-value transactions that must be confirmed before delivering the resource |
| Async | false | Returns txHash immediately after submitting, without waiting for confirmation | Medium value, with response-speed requirements |
Async settlement carries a timing risk: the resource may be delivered before the on-chain payment is finally confirmed. Sync settlement is recommended for high-value transactions. The
chargescheme supports sync only (the default and only option).
charge path#
charge doesn't use x402's accepts array; instead it describes each route's price with a ChargeConfig type + MppCharge<T> extractor — the price is returned by an associated function of the type and locked at compile time, so it can't be overridden by the route-config layer.
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// Start: node --env-file=.env --experimental-strip-types server.ts
// Or: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
// SA-API client (broadcasts EIP-3009 in transaction mode).
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: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// Per-route price (base units; "100" = 0.0001 of a 6-decimal token).
// fee_payer = true → seller broadcasts the EIP-3009 (transaction mode).
const CHARGE = {
amount: "100",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // receipt
description: "One premium API call",
methodDetails: { chainId: 196, feePayer: true }, // X Layer
} as const;
// Runs only after verify + settle.
async function premium(request: Request): Promise<Response> {
const result = await mppx.charge(CHARGE)(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "premium content" }));
}
// node:http ↔ Web Standards bridge (10 lines).
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/api/premium"
? await premium(webReq)
: new Response("not found", { status: 404 });
res.statusCode = webRes.status;
webRes.headers.forEach((v, k) => res.setHeader(k, v));
res.end(await webRes.text());
}).listen(4000);
upto path#
upto is a variant of one-time payment — the buyer first authorizes a payment cap, and after the server completes the request it settles the actual amount (≤ cap) based on the real cost; the over-authorized portion is not charged. If the request ultimately has no billable output, the actual amount can be 0, in which case no on-chain transaction is initiated at all.
Suitable for scenarios that are a single call but whose final amount can only be determined after the request finishes executing, for example: a data query billed by the number of rows actually returned, or a batch validation billed by the number of entries actually processed successfully.
Key differences from exact:
priceis a cap, not the amount actually charged; the real amount is filled in by the handler after the request is processed.uptois based on Permit2 signing (not EIP-3009), but the settlement structure is still "one settlement per request".
upto is based on Permit2 signing; buyers can use any EVM wallet. As with exact + Permit2, the buyer must do a one-time approve for that token (authorizing the Permit2 contract) on first use.
npm install express @okxweb3/x402-express @okxweb3/x402-core @okxweb3/x402-evm
npm install -D typescript tsx @types/express @types/node
import express, { Request, Response } from "express";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
paymentMiddleware,
setSettlementOverrides,
x402ResourceServer,
} from "@okxweb3/x402-express";
import { UptoEvmScheme } from "@okxweb3/x402-evm/upto/server";
const facilitator = new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
const resourceServer = new x402ResourceServer(facilitator)
.register("eip155:196", new UptoEvmScheme());
// price is the cap, not the actual charge; the handler decides the actual amount.
const routes = {
"GET /api/usage": {
accepts: [
{
scheme: "upto",
network: "eip155:196",
payTo: process.env.PAY_TO_ADDRESS!,
price: "$0.10", // The cap the buyer authorizes
maxTimeoutSeconds: 300,
// extra omitted — UptoEvmScheme auto-injects the required fields
},
],
description: "Pay-by-actual-usage demo (upto)",
mimeType: "application/json",
},
};
const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, resourceServer));
app.get("/api/usage", (_req: Request, res: Response) => {
// Compute the real cost of this request (e.g. tokens × unit price)
const actualAmount = "$0.034";
// The middleware reads this header, settles the override amount, and strips the header before responding.
// amount accepts four formats:
// "1234000" base units
// "50%" percent of the cap
// "$0.034" dollar string (same syntax as price)
// "0" short-circuit — no on-chain transaction
setSettlementOverrides(res, { amount: actualAmount });
res.json({ report: { tokens_used: 1342, model: "demo" }, billed: actualAmount });
});
app.listen(4000, async () => {
await resourceServer.initialize();
});
uptois currently supported only on the Node.js / Rust SDKs; Go / Java / Python are coming soon.
Advanced#
Applies to HTTP sellers only — the SDK code blocks below all mount on HTTP middleware; Agent sellers generate payment links via the Skill and don't touch this layer.
1. Splits (only charge)#
A single payment is automatically split to up to 10 recipients. Common scenarios: platform commission, multi-party revenue sharing, referral commissions.
Hard constraints:
sum(splits.amount) < ChargeConfig::amount()(the total of splits must be strictly less than the primary amount)splits.len() ≤ 10- Each
recipientmust be an EIP-55 checksummed 40-hex address
Signing overhead: the buyer signs one EIP-3009 per split — 1 for the primary recipient + 1 per split — all submitted together by the seller at /settle.
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// Start: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
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: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// Total 100 base units; primary keeps 50, splits take 30 + 20.
// Constraints: sum(splits) < amount; splits.length <= 10;
// recipient must be 40-hex EIP-55.
// Buyer signs one EIP-3009 per split (one to primary + one per entry).
const splits = [
{ amount: "30", recipient: "0x....321a1308", memo: "partner-a" },
{ amount: "20", recipient: "0x....d31a6608", memo: "partner-b" },
];
// Only difference vs single charge: methodDetails.splits.
const CHARGE = {
amount: "100",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // primary receipt
description: "One premium API call (split)",
methodDetails: { chainId: 196, feePayer: true, splits },
} as const;
async function premium(request: Request): Promise<Response> {
const result = await mppx.charge(CHARGE)(request);
if (result.status === 402) return result.challenge;
return result.withReceipt(Response.json({ data: "premium content" }));
}
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/api/premium"
? await premium(webReq)
: new Response("not found", { status: 404 });
res.statusCode = webRes.status;
webRes.headers.forEach((v, k) => res.setHeader(k, v));
res.end(await webRes.text());
}).listen(4000);
charge split is currently implemented as explicit amounts — each split is an absolute base-units value, not a ratio.
2. Supporting exact + charge simultaneously#
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/payment-router": "^0.1.0",
"@okxweb3/mpp": "^0.1.0",
"@okxweb3/x402-core": "^0.1.0",
"@okxweb3/x402-evm": "^0.1.0"
}
}
// server.ts
// Start: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { Mppx } from "@okxweb3/mpp";
import { charge as mppCharge } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
import { OKXFacilitatorClient } from "@okxweb3/x402-core";
import {
x402HTTPResourceServer,
x402ResourceServer,
} from "@okxweb3/x402-core/server";
import { ExactEvmScheme } from "@okxweb3/x402-evm/exact/server";
import {
MppAdapter,
X402Adapter,
paymentRouter,
} from "@okxweb3/payment-router";
// —— MPP setup ——
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: [mppCharge({ saClient })],
realm: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// —— x402 setup (facilitator + scheme; routes are declared on the router) ——
const NETWORK = "eip155:196"; // X Layer Mainnet
const x402Server = new x402ResourceServer(
new OKXFacilitatorClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
}),
).register(NETWORK, new ExactEvmScheme());
// Built-in priorities: MPP=10, x402=20 (MPP wins when both headers present).
// Custom adapters should start at priority ≥ 100.
const protect = paymentRouter({
adapters: [
new MppAdapter({ mppx }),
new X402Adapter({
resourceServer: x402Server,
httpResourceServerCtor: x402HTTPResourceServer,
}),
],
routes: {
"GET /generateImg": {
description: "AI Image Generation Service",
adapterConfigs: {
mpp: {
intent: "charge",
amount: "10000",
currency: "0x...adb21711", // currency
recipient: "0x...378211", // receipt
description: "AI Image Generation Service",
methodDetails: { chainId: 196, feePayer: true },
},
x402: {
scheme: "exact",
network: NETWORK,
payTo: "0x...378211", // receipt
price: "$0.01",
description: "AI Image Generation Service",
mimeType: "application/json",
},
},
},
},
});
// Protocol-agnostic. Runs only after one of the adapters has verified payment.
const handler = protect(async () =>
Response.json({
imageUrl: "https://placehold.co/512x512/png?text=AI+Generated",
prompt: "a sunset over mountains",
}),
);
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4000"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const webRes =
new URL(url).pathname === "/generateImg" && req.method === "GET"
? await handler(webReq)
: new Response("not found", { status: 404 });
res.statusCode = webRes.status;
webRes.headers.forEach((v, k) => res.setHeader(k, v));
res.end(await webRes.text());
}).listen(4000);
Agent Seller integration#
Agent generates payment links in dialogue#
Agent Sellers don't "passively mount middleware and wait for buyer requests" — instead, the Agent actively generates a payment link in the dialogue when it needs to charge, and sends it through a messaging channel (XMTP / Telegram / etc.).
This section focuses on payment-link generation and delivery; for how two Agents establish a session through Telegram / XMTP / etc., see Quickstart · I'm an Agent Seller — Configure messaging channel gateway.
- 1Install the OnchainOS Skill
Send the following prompt to your AI Agent and follow the guided steps to finish installation:
textPlease help me install the Onchain OS Payment Skill so my Agent can generate payment links and charge externally. My receiving wallet address: 0xYourSellerWalletSee the Agent Seller Quickstart.
- 2Generate a payment link
When it needs to charge, the seller Agent calls the Skill to generate a one-time payment link:
textBuyer Agent: Please translate this 3000-word document for me. Seller Agent: Sure, translation service is 50 USD₮0. [Skill call: createPayment({type:'charge', amount:'50000000', recipient:'0x...'})] Payment link: https://pay.okx.com/p/a2a_01HZX8Q9RK3JWYV7M2N5T8P4ABEach link is one-time and expires automatically after 30 minutes by default.
- 3Send the payment link
Send the payment URL returned by the Skill (in the form
https://pay.okx.com/p/a2a_xxx) to the buyer Agent as a text message; the other side parses the URL and completes signing via Agentic Wallet. - 4Poll payment status
The Skill automatically polls
GET /payment/{paymentId}/status; once the status becomescompleted, it notifies the Agent to deliver the service.textSkill: Payment completed (tx: 0xabc...) Agent: Payment received, starting translation...
Buyer integration#
Buyers integrate by installing the Onchain OS Skill on their Agent — installing the Skill automatically configures Agentic Wallet as the underlying signing wallet, with no separate installation needed. The Skill automatically detects HTTP 402 responses or payment URLs in message channels, completes signing via the wallet, then replays the request — no manual buyer involvement at any point. For the full integration steps, see Agent Buyer.
Limits and trade-offs#
- Tiny single-call price + ultra-high call frequency: per-call on-chain settlement is uneconomical → use Batch payment
- Long-running relationship + repeated cumulative billing (subscription APIs / Agent multi-step tasks): one channel beats per-call settlement → use Pay-as-you-go
- Single call, cost only known afterward: just use
uptoon this page; only "charging accumulated across many calls" needs Pay-as-you-go - Mutually distrustful parties needing acceptance before payout: use Escrow payment
Next#
- When it fitsBusiness flowHTTP Seller integrationChoosing exact, charge, or upto?SDK statusexact pathexact + Permit2: support any ERC-20Sync vs. async settlementcharge pathupto pathAdvanced1. Splits (only charge)2. Supporting exact + charge simultaneouslyAgent Seller integrationAgent generates payment links in dialogueBuyer integrationLimits and trade-offsNext
