Pay-as-you-go#

The buyer first deposits funds into an on-chain escrow account; each call deducts from it, and any unused balance is refunded when the relationship ends. Unlike one-time payments where every call goes on-chain, Pay-as-you-go only touches the chain when opening the account and settling — the countless calls in between happen entirely off-chain. It fits scenarios that need long-running cumulative billing: Agent multi-step task chains, long chat sessions, subscription APIs.

For definitions and the underlying protocol, see Core Concepts · Pay-as-you-go.


When it fits#

Your businessSuitable
Naturally segmented business, fixed unit price per segment (per call / per message / per sub-task)
Long-running, repeated cumulative billing (subscriptions, Agent tasks chaining multiple paid calls)
Buyer wants "pay for what you use" with refundable residual

How it works#

Pay-as-you-go runs in three steps:

StepOne-liner
① Pre-depositThe buyer makes a one-shot deposit into an on-chain escrow account. This is the only mandatory on-chain action.
② Use and signOn every call, the buyer signs a cumulative bill (Voucher) stating "cumulative spend so far is X" (off-chain, instant, 0 gas). The seller stores the latest one locally.
③ SettleAt any point, the seller submits the latest Voucher on-chain to draw the earned amount from the escrow account; the residual balance is auto-refunded to the buyer.

The full flow (with retry, top-up, forced close, and other failure branches) is in Full business flow below.


Terms you'll encounter#

The following four terms recur throughout the rest of this page; build intuition here first.

Channel#

The on-chain escrow account the buyer pre-deposits funds into (held by the Escrow contract — no one can move funds against the rules). Why: it wraps "long-running relationship" into a single object — open it once, and all subsequent calls accumulate inside, avoiding per-call on-chain settlement.

Voucher (cumulative bill)#

The "cumulative spend so far is X" bill the buyer signs on every call (EIP-712 signature).

Four design properties:

PropertyMeaning
Cumulative amount (not incremental)A Voucher states "cumulative spend so far is X", not "this call deducts Y"
Replay-resistantcumulativeAmount increases monotonically; older Vouchers are naturally rejected by the on-chain contract
Loss-tolerantEven if intermediate Vouchers are lost, as long as the seller holds the latest one, settlement still pays the full amount
Zero on-chainVouchers never go on-chain; the seller verifies signatures locally

Why cumulative instead of incremental: the cumulative structure solves two problems at once — (1) natural replay defense (older Voucher amounts are necessarily smaller than the latest, so the on-chain contract rejects them outright); (2) loss tolerance (intermediate ones being lost doesn't matter — final settlement only looks at the latest).

⚠️ The seller must persist every Voucher — see Persisting the Voucher for specifics.

settle vs. close#

OperationChannel stateFund flow
settle (mid-stream)Channel stays openSeller pulls earned amount from the escrow account; remaining funds stay locked
close (terminate)Channel reaches terminal stateFinal settlement + unused balance refunded to the buyer

Grace period#

If the buyer wants to unilaterally close the channel mid-stream, it triggers a 15-minute countdown window, giving the seller time to submit the latest bill on-chain and settle. This is hard-coded at the contract layer; sellers don't configure it. Deep dive in Channel lifecycle · Forced close.


Full business flow#

Entering the technical-detail zone. The above is the intuitive version; here we expand all edge branches — HTTP 402 retry, top-ups, forced close, last-minute saves during the grace period, and so on.

How to read the diagram:

  • Phase 1 — HTTP 402 Challenge guides the buyer through EIP-3009 authorization + on-chain open
  • Phase 2 — HTTP 402 retry model: every call goes through "request → 402 + Challenge → sign Voucher → retry"; each Voucher is a single EIP-712 signature with monotonically increasing cumulativeAmount
  • Phase 3 — three settlement paths coexist: mid-stream settle / cooperative close / forced requestClose

Channel lifecycle#

State machine#

OPEN (in normal use) → CLOSING (buyer initiates requestClose, grace period starts) → CLOSED (grace period expires / seller or buyer completes final settlement).

Three settlement paths#

  • Mid-stream settle: channel stays OPEN; the seller submits the latest Voucher on-chain → the contract disburses from the escrow account → the residual stays locked, future calls continue.
  • Cooperative close: the buyer co-signs a final Voucher → the seller calls close → channel reaches terminal state CLOSED → residual refunded to the buyer. The cleanest close path.
  • Forced close: see "Forced close" below.

Forced close#

What is the grace period#

The grace period is the Escrow contract's delayed-close protection window — when the buyer unilaterally initiates a forced close, the channel doesn't shut down immediately. It first enters the CLOSING state with a 15-minute countdown, giving the seller a chance to front-run on-chain settlement with the latest Voucher. Without this window, a buyer could close the channel the instant before the seller submits the latest Voucher on-chain, turning that Voucher into worthless paper.

The 15 minutes is hard-coded at the contract layer; the seller doesn't configure anything.

Process#

The buyer can unilaterally call requestClose to trigger:

  1. The channel enters the grace period — state becomes CLOSING, the 15-minute countdown begins
  2. During the grace period, the seller can front-run with close using the latest Voucher
  3. After the grace period expires, the buyer calls withdraw to reclaim the residual; channel reaches terminal state CLOSED

The grace period is fixed at 15 minutes. Once the buyer initiates a forced close, the seller has exactly 15 minutes to submit the latest Voucher on-chain. After the window expires, any signed-but-unsettled Voucher amount becomes unrecoverable.

Mitigation: proactively call settle on a regular cadence to settle signed Vouchers on-chain — for example, trigger every N calls or run a scheduled job every few minutes. Higher frequency means a smaller per-period unsettled balance, capping potential loss even when the grace period is missed.

Persisting the Voucher#

⚠️ The SDK default stores the latest Voucher in memory and loses it on process restart. If the server restarts between two settle calls, the latest Voucher is gone and only the older, smaller one can be submitted on-chain — the "latest cumulative − prior cumulative" delta cannot be recovered at the contract layer and constitutes a direct loss.

Mandatory step: swap the SDK's store for persistent storage (Redis / Postgres / SQLite / file store all work). The SDK exposes with_store(...) for injection — see Seller integration.

Topping up when funds run low#

After the channel is open, the buyer can call topUp at any time to add to the deposit; channelId stays the same — no need to re-open. Voucher cumulative amounts can keep growing.

Recommendations
  • Long-running channel (monthly subscription, long-term subscription): we recommend periodic mid-stream settlement to avoid holding overly valuable Vouchers
  • Business clearly ends (buyer cancellation, session ends): proactively close the channel so the buyer gets the residual back promptly

Seller integration#

SDK status#

SchemeNode.jsRustGoJava
session (metered channel)Coming soon

Full code#

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@okxweb3/mpp": "^0.1.0",
    "viem": "^2.21.0"
  }
}
typescript
// 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);

Field reference#

EvmSessionMethod / SessionMethodDetails:

FieldMeaningNotes
with_escrowEscrow contract addressRequired; channel funds are locked in this contract
with_signerSeller signerAccepts any alloy::signers::Signer: PrivateKeySigner / AwsSigner / LedgerSigner / custom remote signer
verify_payeeStartup-time check that signer.address() == expected recipientFail-fast at startup beats account drift at runtime
currencyPricing token contract addressRequired; currently only USDG / USD₮0 and other EIP-3009-compatible stablecoins are supported
recipientPrimary payee addressRequired, EIP-55-checksummed 40-hex address
chain_idChain ID196 = X Layer
fee_payerSome(true) Seller pays gas (transaction mode)Recommended true so buyers don't need to hold X Layer gas
min_voucher_deltaMinimum increment per Voucher (base units)"0" accepts any; raising it lowers the verify cost of high-frequency tiny Vouchers
unit_typeBilling unit nameFree-form: request / message / subtask
unit_priceUnit price (base units)6-decimal stablecoin: "100" = 0.0001
suggested_depositRecommended pre-deposit (base units)Typically unit price × 100, covering one session's worth
realmNamespace isolationUse distinct realms per business line to prevent credential cross-use
secret_keySeller's key for signing ChallengesInject via MPP_SECRET_KEY env var, never hardcode

Buyer integration#

Two paths, branching on whether you have Agentic Wallet installed — both cover the same 4 actions (open / topUp / submit Voucher / close); the difference is "talk to the Agent in natural language" vs. "write client code yourself".

Path A: With Agentic Wallet (recommended)#

The Skill bundled with Agentic Wallet already knows how to invoke the 4 actions; buyers can complete them in one sentence:

What you wantTell the AgentWhat the Skill does
Open the channel"Open Pay-as-you-go for [service], pre-deposit X"EIP-3009 authorization → Broker submits open
Call the service"Use [service] to do X"Auto-signs the Voucher and attaches it to the request
Top up"Add X more to the [service] channel"topUp(channelId, amount)
Close the channel"Close the [service] channel, refund the residual"Cooperative close (or requestClose to enter the 15-minute grace period)

Private key inside TEE, Skill auto-signs; on X Layer USDG / USD₮0 gas is covered by OKX — 0 gas.

Path B: With a regular EVM wallet#

Any wallet that supports EIP-712 and EIP-3009 signing works, but you need to implement client-side code for the 4 actions yourself — open / voucher / topUp / close all go through the HTTP 402 challenge retry model, with the signed credential always carried in the same request header:

Authorization: Payment <base64url(JCS-canonicalized JSON envelope)>

For signature construction details (EIP-712 Voucher / EIP-3009 transferWithAuthorization typed data), see the protocol spec.

Below are code examples:

① open (open the channel)#

Business request curl:

bash
curl -i 'https://api.example.com/v1/chat/completions' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJvcGVuIiwiYXV0aG9yaXphdGlvbiI6eyJmcm9tIjoiMHgxMjM0NTY3ODkwYUJjRGVmMTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4Iiwibm9uY2UiOiIweGExYjJjM2Q0ZTVmNjA3MTgyOTNhNGI1YzZkN2U4ZjkwYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOTAiLCJ0byI6IjB4ZmYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMSIsInR5cGUiOiJlaXAtMzAwOSIsInZhbGlkQWZ0ZXIiOiIwIiwidmFsaWRCZWZvcmUiOiIxNzQ2NjE5MjAwIiwidmFsdWUiOiIxMDAwMDAifSwiY2hhbm5lbElkIjoiMHg2ZDBmNGZkZjFmMmY2YTFmNmMxYjBmYmQ2YTdkNWMyYzBhOGQzZDdiMWY2YTljMWIzZTJkNGE1YjZjN2Q4ZTlmIiwiY3VtdWxhdGl2ZUFtb3VudCI6IjAiLCJzYWx0IjoiMHg5YThiN2M2ZDVlNGYzYTJiMWMwZDllOGY3YTZiNWM0ZDNlMmYxYTBiOWM4ZDdlNmY1YTRiM2MyZDFlMGY5YThiIiwic2lnbmF0dXJlIjoiMHhhYjAxY2QyM2VmNDU2Nzg5MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjFjIiwidHlwZSI6InRyYW5zYWN0aW9uIiwidm91Y2hlclNpZ25hdHVyZSI6IjB4MTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzMWIifSwic291cmNlIjoiZGlkOnBraDplaXAxNTU6MTk2OjB4MTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4OTBhQmNEZWYxMjM0NTY3OCJ9'

Decoded envelope plaintext:

jsonc
{
  "challenge": {
    // Full echo of the seller's WWW-Authenticate (6 fields)
    "id": "kM9xPqWvT2nJrHsY4aDfEb",        // Server's unique challenge identifier
    "realm": "api.example.com",            // Seller domain — HMAC validation scope
    "method": "evm",                       // EVM-chain payment method
    "intent": "session",                   // session ↔ charge — pick one
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",  // Seller's original request params (amount/currency/methodDetails…) base64url'd
    "expires": "2026-05-07T12:00:00Z"      // Expiry (RFC 3339)
  },
  "source": "did:pkh:eip155:196:0x1234567890aBcDef1234567890aBcDef12345678",
                                           // Payer DID — fixed format did:pkh:eip155:<chainId>:<addr>
  "payload": {
    "action": "open",                      // Phase discriminator — one of open/voucher/topUp/close
    "type": "transaction",                 // transaction = seller pays gas; hash = client already broadcast
    "channelId": "0x6d0f4fdf...e9f",       // Deterministically derived on-chain, computed at open time
    "salt": "0x9a8b...9a8b",                // Random salt used to derive channelId
    "authorization": {                     // EIP-3009 deposit authorization (transaction mode only)
      "type": "eip-3009",                  // Constant
      "from": "0x1234...5678",             // The addr in source
      "to": "0xff00...0001",               // Escrow contract
      "value": "100000",                   // Deposit amount in atomic units (USDC 100000 = 0.1 USDC)
      "validAfter": "0",
      "validBefore": "1746619200",         // EIP-3009 signature expiry (Unix seconds)
      "nonce": "0xa1b2...8f90"              // EIP-3009 nonce, derived per the contract formula
    },
    "signature": "0xab01...ef1c",          // EIP-3009 signature over the authorization above
    "cumulativeAmount": "0",               // Voucher cumulative — defaults to "0"
    "voucherSignature": "0x1122...331b"    // EIP-712 signature for Voucher(channelId, cum=0)
  }
}

② voucher (per business call)#

Business request curl:

bash
curl -i 'https://api.example.com/v1/chat/completions' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJ2b3VjaGVyIiwiY2hhbm5lbElkIjoiMHg2ZDBmNGZkZjFmMmY2YTFmNmMxYjBmYmQ2YTdkNWMyYzBhOGQzZDdiMWY2YTljMWIzZTJkNGE1YjZjN2Q4ZTlmIiwiY3VtdWxhdGl2ZUFtb3VudCI6IjUwMDAiLCJzaWduYXR1cmUiOiIweGRlYWRiZWVmMDAxMTIyMzM0NDU1NjY3Nzg4OTkwMGFhYmJjY2RkZWVmZjAwMTEyMjMzNDQ1NTY2Nzc4ODk5YWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTlhYWJiY2NkZGVlZmYwMDExMjIzMzQ0NTU2Njc3ODg5OTExYiJ9fQ'

Decoded envelope plaintext:

jsonc
{
  "challenge": {
    // Same structure as open — echoes this voucher challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "payload": {
    "action": "voucher",                    // Phase discriminator — voucher
    "channelId": "0x6d0f4fdf...e9f",        // Must be the same channelId returned at open
    "cumulativeAmount": "5000",             // Cumulative value (atomic units), strictly greater than the prior voucher
    "signature": "0xdeadbeef...11b"         // EIP-712 Voucher(channelId, cum) signature
  }
}

③ topUp (add to deposit)#

Business request curl:

bash
curl -i -X POST 'https://api.example.com/session/manage' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJ0b3BVcCIsImFkZGl0aW9uYWxEZXBvc2l0IjoiNTAwMDAiLCJhdXRob3JpemF0aW9uIjp7ImZyb20iOiIweDEyMzQ1Njc4OTBhQmNEZWYxMjM0NTY3ODkwYUJjRGVmMTIzNDU2NzgiLCJub25jZSI6IjB4YzNkNGU1ZjYwNzE4MjkzYTRiNWM2ZDdlOGY5MDAxYTFiMmMzZDRlNWY2MDcxODI5M2E0YjVjNmQ3ZThmOTAwMSIsInRvIjoiMHhmZjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxIiwidHlwZSI6ImVpcC0zMDA5IiwidmFsaWRBZnRlciI6IjAiLCJ2YWxpZEJlZm9yZSI6IjE3NDY2MTk1MDAiLCJ2YWx1ZSI6IjUwMDAwIn0sImNoYW5uZWxJZCI6IjB4NmQwZjRmZGYxZjJmNmExZjZjMWIwZmJkNmE3ZDVjMmMwYThkM2Q3YjFmNmE5YzFiM2UyZDRhNWI2YzdkOGU5ZiIsInNpZ25hdHVyZSI6IjB4ZmVlZGZhY2UxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OTAwMTEyMjMzNDQ1NTY2Nzc4ODk5MDAxYiIsInRvcFVwU2FsdCI6IjB4YjFjMmQzZTRmNWE2MDcxODI5MzA0MTUyNjM3NDg1OTZhN2I4YzlkMGUxZjIwMzE0MjUzNjQ3NTg2OTcwOGE5YiIsInR5cGUiOiJ0cmFuc2FjdGlvbiJ9LCJzb3VyY2UiOiJkaWQ6cGtoOmVpcDE1NToxOTY6MHgxMjM0NTY3ODkwYUJjRGVmMTIzNDU2Nzg5MGFCY0RlZjEyMzQ1Njc4In0'

Decoded envelope plaintext:

jsonc
{
  "challenge": {
    // Same structure as open — echoes this topUp challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "source": "did:pkh:eip155:196:0x1234567890aBcDef1234567890aBcDef12345678",
  "payload": {
    "action": "topUp",                     // Phase discriminator
    "type": "transaction",                 // transaction = seller pays gas; hash = client already broadcast
    "channelId": "0x6d0f4fdf...e9f",       // Must be the same channelId from open
    "topUpSalt": "0xb1c2...0a9b",          // 32-byte random salt, used to derive the EIP-3009 nonce
    "authorization": {                     // EIP-3009 deposit authorization
      "type": "eip-3009",                  // Constant
      "from": "0x1234...5678",             // Payer
      "to": "0xff00...0001",               // Escrow contract
      "value": "50000",                    // = additionalDeposit (atomic units, must match)
      "validAfter": "0",
      "validBefore": "1746619500",         // Signature expiry (Unix seconds)
      "nonce": "0xc3d4...9001"              // Derived from the formula above (not random)
    },
    "signature": "0xfeed...001b",          // EIP-3009 signature over the authorization above
    "additionalDeposit": "50000"            // This top-up amount (must equal authorization.value)
  }
}

④ close (close the channel)#

Business request curl:

bash
curl -i -X POST 'https://api.example.com/session/manage' \
  -H 'Authorization: Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjYtMDUtMDdUMTI6MDA6MDBaIiwiaWQiOiJrTTl4UHFXdlQybkpySHNZNGFEZkViIiwiaW50ZW50Ijoic2Vzc2lvbiIsIm1ldGhvZCI6ImV2bSIsInJlYWxtIjoiYXBpLmV4YW1wbGUuY29tIiwicmVxdWVzdCI6ImV5SnBiblJsYm5RaU9pSnpaWE56YVc5dUlpd2lZVzF2ZFc1MElqb2lNVEF3TUNJc0ltTjFjbkpsYm1ONUlqb2lNSGczTkdJM1pqRTJNek0zWWpoa1pqVmtPVEpqT1dZNFkySmpabUpqWkdRNU1EQTBPV1ZqTW1Zd0lpd2ljbVZqYVhCcFpXNTBJam9pTUhnM1pUSm1NMk0wWkRWbE5tWTNZVGhpT1dNd1pERmxNbVl6WVRSaU5XTTJaRGRsT0dZNVlUQmlJaXdpYldWMGFHOWtSR1YwWVdsc2N5STZleUpqYUdGcGJrbGtJam94T1RZc0ltVnpZM0p2ZDBOdmJuUnlZV04wSWpvaU1IaG1aakF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNUkUlPSlxImZWVbDXBoZpwl1lqcDBjblZsZlgwIfWslmF5bG9hZCk6cyJhY3Rpb24iOiJjbG9zZSIsImNoYW5uZWxJZCI6IjB4NmQwZjRmZGYxZjJmNmExZjZjMWIwZmJkNmE3ZDVjMmMwYThkM2Q3YjFmNmE5YzFiM2UyZDRhNWI2YzdkOGU5ZiIsImN1bXVsYXRpdmVBbW91bnQiOiI0MjAwMCIsInNpZ25hdHVyZSI6IjB4Y2FmZWJhYmU5OTg4Nzc2NjU1NDQzMzIyMTEwMDk5YWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTlhYWJiY2NkZGVlZmYwMDExMjIzMzQ0NTU2Njc3ODg5OWFhYmJjY2RkZWVmZjAwMTEyMjMzNDQ1NTY2Nzc4ODk5MjFjIn19'

Decoded envelope plaintext:

jsonc
{
  "challenge": {
    // Same as above — echoes the seller's last challenge
    "id": "kM9xPqWvT2nJrHsY4aDfEb",
    "realm": "api.example.com",
    "method": "evm",
    "intent": "session",
    "request": "eyJpbnRlbnQiOiJzZXNzaW9uIi...",
    "expires": "2026-05-07T12:00:00Z"
  },
  "payload": {
    "action": "close",                      // Phase discriminator — close
    "channelId": "0x6d0f4fdf...e9f",        // Must be the same channelId from open
    "cumulativeAmount": "42000",            // = current_cum, the highest cumulative within the session
    "signature": "0xcafebabe...21c"         // EIP-712 final Voucher signature
  }
}

Gas fees#

On X Layer, USDG / USD₮0 gas is covered by OKX — 0 gas on both paths.


Limits and trade-offs#

When not to use Pay-as-you-go
  • Price is fixed and known: One-time payment is a direct fit; the pre-deposit + cumulative model is unnecessary here
  • Tiny unit price + ultra-high frequency: Batch payment's Session Key + TEE model is purpose-built for this scenario
  • Buyer only makes one or two calls: Pay-as-you-go assumes an ongoing relationship; for one-shot calls use One-time payment
  • Need escrow release (no payout until task delivered): use Escrow payment

Agent Seller (coming soon)#

The Agent Seller version is coming soon. The Agent Seller scenario is carried by OKX as an extension on top of the protocol (independent from HTTP Seller at the underlying layer), but the semantic layer (Challenge / Credential) and field structure stay consistent with HTTP Seller.

DimensionHTTP SellerAgent Seller (coming soon)
Challenge carrierHTTP 402 responseMessaging channel message body
Channel-open triggerClient's first requestAgent initiates within the dialogue
Voucher submissionHTTP request headerMessage reply
Business driverAPI callAgent dialogue

Next#