# 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](https://github.com/coinbase/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](/payments/facilitator).

## If you run `@ixo/decision-oracle`

The [decision oracle](/build-an-oracle/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

```mermaid
sequenceDiagram
  participant C as Client
  participant O as Oracle
  participant F as Facilitator
  participant E as Decision engine
  O->>F: POST /offers
  F-->>O: 201 offerId, paymentRequired
  O-->>C: 402, PAYMENT-REQUIRED header
  C->>O: retry with PAYMENT-SIGNATURE header
  O->>F: POST /verify
  F-->>O: isValid true (price reserved, no money moved)
  O->>E: claim
  E-->>O: decision and signed receipt
  O->>F: POST /settle with receipts
  F-->>O: settlement_pending, then success with the transaction
```

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

```http
POST /offers
```

```json
{
  "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:

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

### Verify

```json
{
  "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:

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

Before the money has moved:

```json
{
  "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:

```bash
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:

  ```ts
  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

| 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-pay`](https://www.npmjs.com/package/@ixo/x402-ixo-pay) on npm: the schemas, the
  header codec, `createPaymentSignature`, `buildPaymentDelegation` and every refusal code.
- [Decision oracle API reference](/build-an-oracle/decision-oracle/api-reference): the oracle's
  side of payments, its vars and its `402` answers.

## Next steps

<CardGroup cols={3}>
  <Card title="IXO Pay Facilitator" icon="credit-card" href="/payments/facilitator">
    Where the money goes for each way to pay.
  </Card>
  <Card title="Decision oracle" icon="scale-balanced" href="/build-an-oracle/decision-oracle">
    Build an oracle that charges per decision.
  </Card>
  <Card title="Host your own payouts facilitator" icon="server" href="/payments/self-host-payouts">
    Pay your company's people from your own Stripe or Base wallet.
  </Card>
</CardGroup>
