Charge for decisions

The x402 payment flow of the IXO Pay Facilitator for oracle builders and API clients: offers, the 402, PAYMENT-SIGNATURE, verify and settle on the decision engine's receipts, with wire examples and the errors that matter.

The IXO Pay Facilitator speaks x402 version 2, the HTTP 402 payment standard. An oracle (the resource server) asks it for an offer, answers its client with 402 Payment Required, and has it verify the client's payment. Once the decision engine has decided, the oracle settles with the engine's signed receipts, and only then does money move.

This page is for two readers: oracle builders who run their own resource server, and API clients that pay an oracle. For where the money goes and what IXO takes, see IXO Pay Facilitator.

If you run @ixo/decision-oracle

The decision oracle package does this whole flow for you. Turn it on with these wrangler.jsonc vars, then redeploy:

VarValue
API_LANEtrue
API_PAYMENTStrue
X402_FACILITATOR_URLhttps://facilitator.ixo.earth (testnet https://facilitator.testnet.ixo.earth, devnet https://facilitator.devnet.ixo.earth)
X402_NETWORKcosmos:ixo-5 (testnet cosmos:pandora-8, devnet cosmos:devnet-1)
EARNINGS_ADDRESSyour ixo1… account, only when IXO's plan for your oracle takes PAY

USDC on Base is paid to the payToEvm address in IXO's plan for your oracle, which IXO sets when it turns USDC on for you; read it at the decision engine's GET /v1/oracles/me. Card is paid to the Stripe account you onboard in the Decision Console. Your price is the pricing.api you publish; the facilitator refuses an offer at any other price. The rest of this page is the wire, for anyone building their own resource server or client.

The flow

  1. POST /offers. The oracle names the claims and their prices. The facilitator checks each price against the oracle's published one and returns the paymentRequired object, listing every way to pay the oracle's plan allows.
  2. 402. The oracle answers the client with 402 and that object, base64-encoded, in the PAYMENT-REQUIRED header.
  3. PAYMENT-SIGNATURE. The client picks one entry of accepts, signs a payment and sends the request again with the PAYMENT-SIGNATURE header.
  4. POST /verify. The facilitator checks the payment and reserves the price for each claim. No money moves.
  5. POST /settle. After the decision, the oracle sends the same body plus one engine receipt per claim (up to 100). Money moves now: in PAY with the next minute's transfer, in USDC when Base marks the transfer safe, by card at capture.
  6. Paid. /settle answers settlement_pending until the money has moved. Send the identical /settle again, or read GET /settlements/:id, to get success: true and the transaction.

A claim that never gets a receipt is released after 24 hours, and nothing is paid. The oracle can release it sooner with POST /v1/reservations/release, or extend it while the claim waits for human review with POST /v1/reservations/extend (up to 30 days).

Who may call

/offers, /verify, /settle, /settlements/:id, extend and release are for resource servers. Each call carries a UCAN invocation as Authorization: Bearer <token>:

  • the audience is the facilitator's did:web (GET /supported lists it under signers);
  • the capability is that one route's action on ixo:ixo-pay-facilitator: x402/offer, x402/verify, x402/settle (also for GET /settlements/:id), x402/extend or x402/release. A wildcard * is refused;
  • it lives for at most one hour.

The caller is admitted when it is a did:web registered with the decision engine with an active plan. A bad or missing token is 401; a caller with no active plan is 403. /supported and /health are public.

Wire examples

These are PAY (ixo-pay) examples on mainnet. Amounts are integer strings in the asset's smallest unit: upay for PAY and micro-USD for USDC and card (1 USD = 1,000,000).

Offer

POST /offers
{
  "resource": { "url": "https://oracle.example/v1/api/claims" },
  "paymentRequirements": {
    "scheme": "ixo-pay",
    "network": "cosmos:ixo-5",
    "asset": "upay",
    "amount": "1200000",
    "payTo": "ixo1…",
    "maxTimeoutSeconds": 120,
    "extra": { "fundingUrl": "https://…" }
  },
  "charges": [
    { "claimId": "apc_…", "sourceId": "…", "protocolDid": "did:ixo:entity:…", "amount": "1200000" }
  ]
}

paymentRequirements carries the PAY terms and your own payee. Leave it out when IXO's plan for your oracle takes no PAY (card or USDC only): the facilitator adds those entries itself. The answer:

{ "offerId": "7c1e2a4b-…", "expiresAt": "…", "paymentRequired": { "x402Version": 2, "resource": { "…": "…" }, "accepts": [ "…" ] } }

Verify

{
  "x402Version": 2,
  "offerId": "7c1e2a4b-…",
  "paymentRequirements": { "scheme": "ixo-pay", "…": "the entry of accepts the client paid" },
  "paymentPayload": "<the PAYMENT-SIGNATURE header, as sent>"
}

→ { "isValid": true, "payer": "did:ixo:ixo1…" }, or { "isValid": false, "invalidReason": "insufficient_credits", "payer": "did:ixo:ixo1…" }. A refusal is still HTTP 200.

Settle

The verify body plus the engine's receipts:

{ "…": "the verify body", "receipts": [{ "claimId": "apc_…", "jws": "<the engine's receipt>" }] }

Before the money has moved:

{
  "success": false,
  "errorReason": "settlement_pending",
  "transaction": "",
  "network": "cosmos:ixo-5",
  "extensions": { "org.udid.receipt@1": { "settlementId": "f66bad2c…", "claims": [{ "claimId": "apc_…", "state": "settled" }] } }
}

The identical body after it: "success": true, the transaction hash, and each claim "state": "paid".

Paying as a client

Install the wire package. It runs on Node 24 LTS or newer, in browsers and in Cloudflare Workers:

npm install @ixo/x402-ixo-pay
  • PAY. Sign a payment delegation once (in the Decision Console, or with buildPaymentDelegation from the package). It is a UCAN to the facilitator with one billing/pay capability, a maxAmount cap and the oracles it may pay; mainnet and testnet refuse one that names no oracles. Then answer each 402 with it:

    import { createPaymentSignature, decodeHeader, PaymentRequired } from "@ixo/x402-ixo-pay";
    
    const required = PaymentRequired.parse(decodeHeader(res.headers.get("PAYMENT-REQUIRED")!));
    const signature = createPaymentSignature(process.env.IXO_PAYMENT_DELEGATION!, required);
    // resend the request with the PAYMENT-SIGNATURE header set to `signature`
  • USDC on Base. The exact entry is x402's standard EVM scheme, so a standard x402 EVM client signs it: an EIP-3009 transferWithAuthorization to the entry's payTo for its amount, valid for the offer's whole window. You need the USDC in your wallet; IXO's relayer pays the gas.

  • Card. Save a card with IXO once. For each offer, call POST /v1/card/authorize { offerId } with your payment delegation as the Bearer token. It places the hold and returns the { paymentIntentId, customerId } that goes in your PAYMENT-SIGNATURE.

Errors that matter

WhereCodeWhat it means
/offers (422)price_mismatchA charge is not the oracle's published price. Its cache catches up within about 5 minutes; retry
/offers (422)plan_inactiveIXO's plan for the oracle takes no payment, or the way to pay is not available to it
/offers (422)review_billing_not_allowedA chain-claim offer, and IXO has not allowed per-review billing for this oracle
/verifyinsufficient_creditsThe payer's PAY balance does not cover the price
/verifyspend_capThe payment delegation's cap, or the send permission, does not cover it
/verifyexpiredThe offer or the delegation has expired
/verifynonce_usedThe offer was already paid with another payment
/verifyinsufficient_fundsUSDC: the payer's wallet does not hold the amount
/verify, /settlereservation_releasedThe reservation lapsed or was released. Do not start, or keep, the claim on it
/settleinvalid_receipt, receipt_mismatchA receipt is missing, not the engine's, or not for this claim. Nothing in the request is settled
/settlesettlement_pendingNot an error: the money has not moved yet. Send the identical body again
any (400)invalid_requestThe body is not the wire format
any (503)The engine, Blocksync, the chain or the UCAN store is unreachable. Retry; it is never a refusal

Reference

Next steps