Payments

Pay per call. Keep the receipt.

Occestra settles per call with x402 v2, scheme exact, on X Layer mainnet (eip155:196). No facilitator, no trusted third party, no API key: the buyer signs an EIP-3009 transfer authorization, and Occestra submits it on-chain itself. This page documents the flow exactly as the gate implements it — the shapes below are the shapes on the wire.

The whole flow in four steps

fieldtypenotes
1 · callPOST /mcpCall a paid tool with no payment attached.
2 · 402challengeHTTP 402. The challenge is the JSON body AND base64 in the PAYMENT-REQUIRED response header.
3 · retryPAYMENT-SIGNATURESign an EIP-3009 transferWithAuthorization for the exact amount, replay the same request with the proof header.
4 · receiptPAYMENT-RESPONSEOccestra verifies the signature itself, claims the nonce, settles on-chain, runs the tool, and returns the result with a settlement receipt header.

Step 2 — the 402 challenge

HTTP 402 body (and base64 of it in the PAYMENT-REQUIRED header)

{
  "x402Version": 2,
  "resource": {
    "url": "https://api.occestra.xyz/mcp",
    "description": "oce_critique",
    "mimeType": "application/json"
  },
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:196",
    "asset": "0x779ded0c9e1022225f8e0630b35a9b54be713736",
    "amount": "10000",
    "payTo": "0x0d63f9EeB86813230B72017444cea16Cd4A453F2",
    "maxTimeoutSeconds": 300,
    "extra": { "name": "USD₮0", "version": "1" }
  }]
}
fieldtypenotes
assetaddressUSD₮0 on X Layer — 6 decimals. Its EIP-712 domain (name/version in `extra`) is verified against the token's on-chain DOMAIN_SEPARATOR.
amountstring, atomicPrice in atomic units: 0.01 USDT → "10000".
payToaddressOccestra's treasury. Your authorization must name exactly this payee.
maxTimeoutSecondsnumberYour validBefore must fall inside this window.

Step 3 — sign and retry

Sign the token's TransferWithAuthorization type (EIP-3009), base64 the proof, retry with the PAYMENT-SIGNATURE header:

pay.mjs — the ~30 lines that answer any Occestra 402

import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_KEY);
const challenge = /* the 402 body's accepts[0] */;

const authorization = {
  from: account.address,
  to: challenge.payTo,
  value: BigInt(challenge.amount),
  validAfter: 0n,
  validBefore: BigInt(Math.floor(Date.now() / 1000) + challenge.maxTimeoutSeconds),
  nonce: `0x${crypto.getRandomValues(new Uint8Array(32)).reduce((s, b) => s + b.toString(16).padStart(2, "0"), "")}`,
};

const signature = await account.signTypedData({
  domain: {
    name: challenge.extra.name,        // "USD₮0"
    version: challenge.extra.version,  // "1"
    chainId: 196,
    verifyingContract: challenge.asset,
  },
  types: {
    TransferWithAuthorization: [
      { name: "from", type: "address" }, { name: "to", type: "address" },
      { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: authorization,
});

const proof = Buffer.from(JSON.stringify({
  x402Version: 2, scheme: "exact", network: "eip155:196",
  payload: { signature, authorization: {
    ...authorization,
    value: authorization.value.toString(),
    validAfter: authorization.validAfter.toString(),
    validBefore: authorization.validBefore.toString(),
  }},
})).toString("base64");

// replay the identical tools/call request, plus:
//   PAYMENT-SIGNATURE: <proof>
Legacy v1 buyers sending the proof as X-PAYMENT are accepted too — the gate reads both headers.

Step 4 — what the gate does with your proof

In order, before any model is touched:

  • 1. Verifies the EIP-712 signature itself against the token's real domain — not against what the proof claims.
  • 2. Checks payee, amount, and time window — the authorization must name Occestra's treasury for the exact price, valid now.
  • 3. Claims the nonce — single-use, persisted; two concurrent requests can never both spend it.
  • 4. Settles — submits transferWithAuthorization on X Layer with its own gas key and waits for inclusion.
  • 5. Runs the tool and returns the result with the receipt header:

PAYMENT-RESPONSE header (base64-decoded)

{
  "status": "settled",
  "transaction": "0x5266b761d94c8e7c83a6f711784b07d79d107111ee00c01f0133fb5d4b7ac5a4",
  "amount": "0.05",
  "payer": "0x…"
}
If settlement reverts, your nonce is released. The nonce is claimed before settling (double-spend safety), but if no money moved — insufficient balance, a bad RPC minute — your signed authorization is still perfectly good, and burning it would force you to re-sign for a payment we merely failed to collect. Retry with the same proof.

That transaction hash above is a real production settlement — the first paid call Occestra ever served. Look it up on the explorer.

Failure modes, honestly

fieldtypenotes
402no/invalid proofThe challenge. Sign and retry.
400bad authorizationWrong payee, wrong amount, expired window, malformed payload — the body says which.
409nonce spentThat authorization was already used. Sign a fresh one.
502settlement failedNo money moved; nonce released; retry safe.
PolicyRefusalbefore paymentBlocked briefs are screened BEFORE the paywall — a refused brief is never charged.