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:
| Var | Value |
|---|---|
API_LANE | true |
API_PAYMENTS | true |
X402_FACILITATOR_URL | https://facilitator.ixo.earth (testnet https://facilitator.testnet.ixo.earth, devnet https://facilitator.devnet.ixo.earth) |
X402_NETWORK | cosmos:ixo-5 (testnet cosmos:pandora-8, devnet cosmos:devnet-1) |
EARNINGS_ADDRESS | your 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
POST /offers. The oracle names the claims and their prices. The facilitator checks each price against the oracle's published one and returns thepaymentRequiredobject, listing every way to pay the oracle's plan allows.- 402. The oracle answers the client with
402and that object, base64-encoded, in thePAYMENT-REQUIREDheader. PAYMENT-SIGNATURE. The client picks one entry ofaccepts, signs a payment and sends the request again with thePAYMENT-SIGNATUREheader.POST /verify. The facilitator checks the payment and reserves the price for each claim. No money moves.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 transfersafe, by card at capture.- Paid.
/settleanswerssettlement_pendinguntil the money has moved. Send the identical/settleagain, or readGET /settlements/:id, to getsuccess: trueand 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 /supportedlists it undersigners); - the capability is that one route's action on
ixo:ixo-pay-facilitator:x402/offer,x402/verify,x402/settle(also forGET /settlements/:id),x402/extendorx402/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
buildPaymentDelegationfrom the package). It is a UCAN to the facilitator with onebilling/paycapability, amaxAmountcap and theoraclesit 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
exactentry is x402's standard EVM scheme, so a standard x402 EVM client signs it: an EIP-3009transferWithAuthorizationto the entry'spayTofor itsamount, 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 yourPAYMENT-SIGNATURE.
Errors that matter
| Where | Code | What it means |
|---|---|---|
/offers (422) | price_mismatch | A charge is not the oracle's published price. Its cache catches up within about 5 minutes; retry |
/offers (422) | plan_inactive | IXO's plan for the oracle takes no payment, or the way to pay is not available to it |
/offers (422) | review_billing_not_allowed | A chain-claim offer, and IXO has not allowed per-review billing for this oracle |
/verify | insufficient_credits | The payer's PAY balance does not cover the price |
/verify | spend_cap | The payment delegation's cap, or the send permission, does not cover it |
/verify | expired | The offer or the delegation has expired |
/verify | nonce_used | The offer was already paid with another payment |
/verify | insufficient_funds | USDC: the payer's wallet does not hold the amount |
/verify, /settle | reservation_released | The reservation lapsed or was released. Do not start, or keep, the claim on it |
/settle | invalid_receipt, receipt_mismatch | A receipt is missing, not the engine's, or not for this claim. Nothing in the request is settled |
/settle | settlement_pending | Not an error: the money has not moved yet. Send the identical body again |
| any (400) | invalid_request | The 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
@ixo/x402-ixo-payon npm: the schemas, the header codec,createPaymentSignature,buildPaymentDelegationand every refusal code.- Decision oracle API reference: the oracle's
side of payments, its vars and its
402answers.