Pay-as-you-go#
For definitions and the underlying protocol, see Core Concepts · Pay-as-you-go. This page focuses on mechanism and flow.
When it fits#
| Your business | Suitable |
|---|---|
| Per-call consumption is unpredictable (LLM per token / data per byte) | ✅ |
| Long-running, high-frequency consumption (subscriptions, streams) | ✅ |
| Buyer wants "pay for what you use" with refundable residual | ✅ |
| Per-call price is fixed and known | ❌ Use One-time payment |
| Tiny unit price + ultra-high frequency | ❌ Use Batch payment |
| Buyer only makes one or two calls | ❌ Channel opening is uneconomical, use One-time payment |
Business flow#
Pay-as-you-go has three phases: open the channel → consume and sign → settle / close.
Key design of cumulative Vouchers#
| Property | Description |
|---|---|
| Cumulative amount (not incremental) | Voucher records "cumulative due X as of now" rather than "deduct Y this time" |
| Anti-replay | cumulativeAmount increases monotonically; older Vouchers are naturally rejected by the on-chain contract |
| Loss-tolerant | Even if some Vouchers are lost in transit, as long as the Seller holds the latest one, full settlement is recoverable |
| Zero on-chain | Vouchers do not go on-chain; local signature verification by the Seller is enough |
Mid-stream settle and channel close#
Pay-as-you-go offers two ways to realize funds:
| Operation | Timing | Channel state | Fund flow |
|---|---|---|---|
| settle (mid-stream) | Any time | OPEN | Pays the Seller per the current latest Voucher; remaining funds stay locked |
| close | Business clearly ended | OPEN → CLOSED | Final settlement + unused residual returned to Buyer |
Channel state machine:
OPEN(normal use) →CLOSING(Buyer triggersrequestClose, enters grace period) →CLOSED(grace ends / Seller or Buyer completes final settlement).
settle is stateless: when calling
settle, the Voucher (cumulativeAmount+ Buyer signature) is uploaded with the request — the server does not rely on a local DB. As long as the Seller can produce the latest Voucher, settlement succeeds.closeis the same, and the contract usesmax(submitted amount, on-chain recorded maximum)to protect Seller revenue.
- Long-lived channel (monthly subscriptions, long-term subscriptions): periodically initiate mid-stream settle to avoid holding too-valuable Vouchers
- Business clearly ended (Buyer cancels, session ends): proactively close the channel so the Buyer gets the residual back promptly
Forced close (Buyer-side)#
What is the grace period#
The grace period is a delayed-close protection window in the Escrow contract — when the Buyer unilaterally triggers a forced close, the channel does not stop immediately. It first enters the CLOSING state and counts down, giving the Seller a chance to preempt with the latest Voucher and settle. Without this window, the Buyer could instantly close the channel before the Seller submits the latest Voucher on-chain, leaving the Seller's Voucher as worthless paper.
Forced close flow#
The Buyer can unilaterally call requestClose to trigger:
- Channel enters the grace period — state changes to
CLOSING, countdown starts - During grace, the Seller can preempt with
closeand settle using the latest Voucher - After grace ends, the Buyer calls
withdrawto recover residual funds; the channel terminal state isCLOSED
The grace period is the Seller's last chance. If you don't monitor channel state for long stretches, the Buyer can unilaterally close and recover funds — including amounts you'd theoretically be entitled to but haven't submitted Vouchers for. Be sure to wire up channel-event listening.
Seller integration#
SDK status#
| Scheme | Node.js | Rust | Go | Java |
|---|---|---|---|---|
session (Pay-as-you-go channel) | ✅ | ✅ | ✅ | 💡 |
✅ Live · 💡 Coming soon
Seller integration for Pay-as-you-go has four steps:
- 1Register session payment config
Declare the billing dimension — billing unit (e.g.
llm_token/byte/second), unit price, recommended pre-deposit amount. - 2Mount middleware on the metered route
Per request, the middleware reads the Voucher, verifies the signature locally, and records cumulative usage. Your business code only needs to return the service result based on usage.
- 3Handle Buyer first-time channel open
When the Buyer hasn't opened a channel, the first request automatically triggers a 402 Challenge guiding the Buyer to deposit into the Escrow contract. Subsequent requests hit the existing channel — no re-opening needed.
- 4Mid-stream settle or close the channel
See Mid-stream settle and channel close above. Choose
settle/closebased on business duration.
Implementation code#
package.json:
{
"type": "module",
"dependencies": {
"@okxweb3/mpp": "^0.1.0",
"viem": "^2.21.0"
}
}
// server.ts
// Run: npx tsx --env-file=.env server.ts
import * as http from "node:http";
import { privateKeyToAccount } from "viem/accounts";
import { Mppx } from "@okxweb3/mpp";
import { session } from "@okxweb3/mpp/evm/server";
import { SaApiClient } from "@okxweb3/mpp/evm";
const UNIT_PRICE_BASE_UNITS = "100"; // 0.0001 of a 6-decimal token
const UNIT_TYPE = "request";
const SUGGESTED_DEPOSIT = "10000"; // 100× unit price
const saClient = new SaApiClient({
apiKey: process.env.OKX_API_KEY!,
secretKey: process.env.OKX_SECRET_KEY!,
passphrase: process.env.OKX_PASSPHRASE!,
});
// viem LocalAccount — replace with WalletClient / KMS / HSM signer in production.
// The session method fast-fails on startup if signer.address !== expected payee.
const sellerSigner = privateKeyToAccount(
process.env.MPP_MERCHANT_PRIVATE_KEY! as `0x${string}`,
);
// Default in-memory store. Pass `store: ...` for SQLite / Redis / Postgres.
const mppx = Mppx.create({
methods: [session({ saClient, signer: sellerSigner })],
realm: "test realm",
secretKey: process.env.MPP_SECRET_KEY!,
});
// Per-route session config. Charged per call; voucher accumulates;
// settle batches on /session/manage close action.
const SESSION = {
amount: UNIT_PRICE_BASE_UNITS,
currency: "0x...adb21711", // currency
recipient: "0x...378211", // receipt
description: "Pay-per-use API",
unitType: UNIT_TYPE,
suggestedDeposit: SUGGESTED_DEPOSIT,
methodDetails: {
chainId: 196, // X Layer
escrowContract: process.env.MPP_ESCROW!, // 40-hex escrow address
feePayer: true,
minVoucherDelta: "0",
},
} as const;
// Routes by `payload.action`: open / voucher / topUp / close.
// mppx.session(...)(request) handles all four uniformly:
// - 402 → challenge response
// - 200 → action-specific result; withReceipt() attaches Payment-Receipt
async function manage(request: Request): Promise<Response> {
const result = await mppx.session(SESSION)(request);
if (result.status === 402) return result.challenge;
// open / topUp / close → empty 204; voucher → resource body.
return result.withReceipt(Response.json({ status: "ok" }));
}
http.createServer(async (req, res) => {
const url = `http://${req.headers.host ?? "localhost:4023"}${req.url}`;
const webReq = new Request(url, {
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
});
const path = new URL(url).pathname;
const webRes =
path === "/session/manage"
? await manage(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(4023);
EvmSessionMethod / SessionMethodDetails field reference:
| Field | Meaning | Notes |
|---|---|---|
with_escrow | Escrow contract address | Required; channel funds are locked in this contract |
with_signer | Seller signer | Accepts any alloy::signers::Signer: PrivateKeySigner / AwsSigner / LedgerSigner / custom remote signer |
verify_payee | Startup-time check that signer.address() == expected recipient | Fail-fast at startup beats account drift at runtime |
currency | Pricing token contract address | Required; currently only USDG / USD₮0 and other EIP-3009-compatible stablecoins are supported |
recipient | Primary payee address | Required, EIP-55-checksummed 40-hex address |
chain_id | Chain ID | 196 = X Layer |
fee_payer | Some(true) Seller pays gas (transaction mode) | Recommended true so Buyers don't need to hold X Layer gas |
min_voucher_delta | Minimum increment per Voucher (base units) | "0" accepts any; raising it lowers the verify cost of high-frequency tiny Vouchers |
unit_type | Billing unit name | Free-form: request / llm_token / byte / second |
unit_price | Unit price (base units) | 6-decimal stablecoin: "100" = 0.0001 |
suggested_deposit | Recommended pre-deposit (base units) | Typically unit price × 100, covering one session's worth |
realm | Namespace isolation | Use distinct realms per business line to prevent credential cross-use |
secret_key | Seller's key for signing Challenges | Inject via MPP_SECRET_KEY env var, never hardcode |
Buyer integration#
Pay-as-you-go does not require Agentic Wallet — any EVM wallet that supports EIP-712 signing works. We recommend Agentic Wallet for the smoothest automation experience.
- 1First request triggers channel open
On the first request, the Buyer receives a 402 Challenge; the wallet prompts "open a pre-deposit channel, deposit X USD₮0". After signing the authorization, the Broker submits the
opentransaction on the Buyer's behalf. - 2Sign Vouchers on each call
The Agent / wallet signs cumulative Vouchers automatically on subsequent requests; the wallet UI clearly shows "you're signing a cumulative bill (cumulative due X USD₮0 as of now)".
- 3Top up and resume
Call
topUpwhenever funds run low — no need to re-open the channel. New sessions can specify an existingchannelIdin the 402 to reuse the same channel. - 4Proactively close
At the end of the business, call
close(cooperative) orrequestClose(forced + grace period); residual funds are auto-refunded.
Limits and trade-offs#
- Fixed and known price: Channel opening costs one on-chain action → use One-time payment
- Tiny unit price + ultra-high frequency: Channel signing / reconciliation overhead is worse than just using Batch payment aggregation
- Buyer only makes one or two calls: Channel opening is uneconomical; One-time payment is lighter
- Need escrow release (no payout before delivery): use Escrow payment
Advanced (already supported at the protocol layer)#
Reuse a channel#
channelId is precomputed by the client (bytes32) from salt and other parameters. After one open, the client holds a channel identified by that channelId; subsequent sessions can keep signing Vouchers on the same channel without re-opening. When funds run low, use topUp instead.
Channel event listening#
On-chain Escrow events triggered unilaterally by the Payer must be consumed, otherwise you'll miss settlement windows or drift in reconciliation:
| Event | Trigger | Priority | Action |
|---|---|---|---|
requestClose | Payer | Very high | Immediately call close and settle using the latest Voucher (grace period countdown has already started) |
withdraw | Payer | High | After grace ends the user withdraws; channel terminal state is CLOSED, sync local state |
Agent Seller (coming soon)#
The Agent Seller version is coming soon. The Agent Seller scenario is carried by an OKX extension on top of the protocol (independent at the underlying layer from HTTP Sellers), but the semantic layer (Challenge / Credential) and field structure remain identical to the HTTP Seller.
| Dimension | HTTP Seller | Agent Seller (coming soon) |
|---|---|---|
| Challenge carrier | HTTP 402 response | Messaging channel message body |
| Channel-open trigger | Client first request | Agent initiates in dialogue |
| Voucher submission | HTTP request header | Message reply |
| Business driver | API calls | Agent dialogue |