One-time Payment#
HTTP Sellers focus on: Business flow → HTTP Seller integration (scheme selection → exact path / charge 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 |
|---|---|
| Single-call amount is fixed and clear (one report / one inference / one query) | ✅ |
| Price is determined before the call, no follow-on consumption after | ✅ |
| Resource is non-revocable (file download, report generation, etc.) | ✅ (recommend syncSettle: true) |
Business flow#
The diagram below is the abstract-level flow — HTTP Sellers are "triggered by client request", Agent Sellers are "triggered by the Agent generating a payment link in the dialogue", but the Challenge / Credential message semantics are identical. The carrier differences are covered in the seller integration sections below.
KYT (on-chain risk screening) is a compliance check inside the Broker's Verify stage — not a separate service.
HTTP Seller integration#
exact or charge?#
exact — single recipient, supports both sync and async settlement.
charge — supports a single recipient too, plus splits, i.e. one payment to multiple addresses (≤10). Defaults to and only supports sync settlement.
| Dimension | exact | charge |
|---|---|---|
| Recipient | Single | Single / one-payment-to-multiple-addresses (≤10) |
| Settlement timing | Sync / async | Default and sync only |
Not sure which to pick? Use supporting exact + charge simultaneously and let the buyer choose.
SDK status#
| Scheme | Node.js | Rust | Go | Java |
|---|---|---|---|---|
exact | ✅ | ✅ | ✅ | ✅ |
charge | ✅ | ✅ | ✅ | Coming soon |
exact path#
Each Tab includes the full install command + implementation code. The architecture has 4 components: Facilitator client (with OKX API Key) → Resource Server (registers the scheme) → Routes config (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 settle: 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");
});
Multi-scheme declarations all go through the
accepts: [...]array; to appendaggr_deferred(Batch payment), just add another entry to the array — see Batch payment.
Sync vs. async settlement#
After calling /settle, the Facilitator submits the transaction on-chain. The syncSettle field in the route config controls settlement behavior:
| Mode | Value | Facilitator behavior | Use case |
|---|---|---|---|
| Sync | true | Submits the transaction and waits for on-chain confirmation before returning txHash | High-value transactions; need confirmation before delivering the resource |
| Async | false | Returns txHash immediately after submission, without waiting for confirmation | Medium-value, response-speed sensitive |
Async settlement carries a timing risk: the resource may be delivered before the on-chain payment is finalized. Sync settlement is recommended for high-value transactions. The
chargescheme defaults to and only supports sync.
charge path#
charge does not use x402's accepts array; instead it uses a ChargeConfig type + MppCharge<T> extractor to describe each route's price — the price is returned by an associated function on the type and locked at compile time, not overridable at the route-config layer.
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// Run: 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);
Advanced#
Applies only to HTTP Sellers — the SDK code blocks below all attach to HTTP middleware. Agent Sellers generate payment links via the Skill and don't touch this layer.
1. Splits (only charge)#
One payment is automatically split among up to 10 recipients. Common scenarios: platform commission, multi-party revenue split, referral payout.
Hard constraints:
sum(splits.amount) < ChargeConfig::amount()(split total must be strictly less than the primary amount)splits.len() ≤ 10- Each
recipientmust be an EIP-55-checksummed 40-hex address
Signing burden: the buyer signs one EIP-3009 per split — 1 to the primary recipient + 1 per split — and the seller submits them all together at /settle.
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0"
}
}
// server.ts
// Run: 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);
The current charge split implementation uses explicit amounts — each split is in absolute base units, not a percentage.
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
// Run: 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 Onchain OS Skill
Send the prompt below to your AI Agent and follow the guided install:
textPlease install the Onchain OS Payment Skill so my Agent can generate payment links and charge externally. My payout wallet address: 0xYourSellerWallet - 2Generate a payment link
The seller Agent calls the Skill to generate a one-time payment link when it needs to charge:
textBuyer Agent: Translate this 3000-word document for me Seller Agent: Sure, translation service 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 URL returned by the Skill (in the form
https://pay.okx.com/p/a2a_xxx) as a text message to the buyer Agent. The other side parses the URL and uses Agentic Wallet to sign. - 4Poll payment status
The Skill polls
GET /payment/{paymentId}/statusautomatically. When 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 in their Agent — installing the Skill automatically configures Agentic Wallet as the underlying signing wallet, no separate install. The Skill auto-detects HTTP 402 responses or payment URLs in the messaging channel, calls the wallet to sign, and replays the request — all without manual intervention. Full integration steps in 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
- Mutually distrustful parties needing acceptance before payout: use Escrow payment