---
title: "Payments"
description: "How money moves through ONE — credits as the unit, the card and crypto rails, x402, escrow custody, and every place a 402 comes from."
type: explanation
audience: both
verified: 2026-09-06
canonical: https://one.ie/docs/payments
---
Money in ONE lands in one of two places: a credit pool held in D1, or a wallet address on a chain. Which one depends on the rail, and the rail decides who ends up holding the funds. This page is for a developer deciding whether to build a paid thing here, and it marks which facts were probed against production and which were read in code.

## Credits are the unit of consumption

A credit pool is a workspace's balance, computed as the sum of `credit_grants` minus the sum of `credit_burns` for that workspace (`one.ie/web/src/lib/credits.ts`). Nothing stores a running total, so a balance is always a derivation over the ledger, and a replayed payment cannot double-grant: grants are inserted `INSERT OR IGNORE` against a unique index on `(rail, rail_ref)`.

Pools nest. A workspace that cannot cover a request walks `owners.parent_slug` upward. `MAX_FUNDING_HOPS` is 5, so at most five workspaces are read — itself and four ancestors — and the first one with enough balance funds the call. That walk is `findFunder`, and it is implemented twice on purpose — once in `one.ie/web/src/lib/credits.ts` for the site, once in `channels/src/credit-gate.ts` for agent turns, because the two workers cannot import across packages. Same walk, same hop limit — the one divergence is that `channels` folds the two balance SUMs into a single round trip, since it pays for the walk on every agent turn.

A burn is priced by tiered brackets over metered usage (`one.ie/web/src/lib/billing/burn.ts`), and the debit path applies the funding walk, a parent markup and a monthly cap before it writes (`debitPool`, `one.ie/web/src/lib/billing.ts`). Pack prices and plan entitlements are deliberately not on this page — a second copy here would drift.

## Two rails fill the pool

The card rail is Stripe. The crypto rail is `pay.one.ie`, which prices a payment against a live rate, names an address, and verifies the transfer on chain before a grant is written. Both close the same way: one row in `credit_grants` tagged with the rail that produced it. The `rail` union in `credits.ts` is `x402 | stripe | sui | evm | solana | btc | one-token | credits`, and `rail_ref` is that rail's own receipt — a Stripe session id, a Sui transaction digest.

A grant also records `test_mode`, derived from Stripe's `event.livemode` at the webhook rather than guessed downstream, so a test purchase stays distinguishable from a real one in every reconciliation total.

## Who the money actually reaches

This is the distinction that decides what you can build.

```
x402 middleware · x402_quote · GET /quote · POST /claim
  → the PLATFORM treasury (env.TREASURY_*), always.
    These calls take no payee. They pay ONE.

payment links (payment_link_create, /l/:payload)
  → the SELLER's own addresses, carried signed in the
    link payload. The seller holds the keys.

checkout:create with rail 'card'
  → a Stripe Connect session on the SELLER's connected
    account, with a platform application fee.
```

`getTreasuryForChainFromEnv` in `pay/backend/src/x402.ts` reads `env.TREASURY_*` and nothing else — no request body, no owner lookup, no per-agent record. That is deliberate. A quote whose destination comes from the request is an unauthenticated call naming where money lands. An agent that wants to be paid to its own address uses the payment-link rail, where the treasuries travel signed in the payload (`pay/backend/src/routes/links.ts`).

The Connect path returns `stripeAccount` and `applicationFeeAmount` from `createCheckout` (`one.ie/web/src/lib/workflow-crud-receivers.ts`). It reports failure as data rather than throwing, because a workflow's `sell` step suspends whether or not the session mints — a seller who has not finished Connect onboarding parks the step instead of crashing the run. The platform selling its own packs is the exception the same function carves out: a platform charge, no connected account, zero application fee (`one.ie/web/src/lib/storefront-session.ts`).

## x402 is a status code, not a checkout page

An unpaid request to a gated route returns `402` with the terms in headers and body. Probed against production on 2026-09-06, abridged — each `payOptions` entry also carries the `address` to pay to, and the body ends with an `instructions` string:

```json
{
  "status": "payment_required",
  "product": "x402-demo",
  "amountUsd": 0.1,
  "payOptions": [
    { "chain": "ETH", "currency": "ETH", "amount": "…" },
    { "chain": "BASE", "currency": "BASE", "amount": "…" },
    { "chain": "SUI", "currency": "SUI", "amount": "…" }
  ]
}
```

Each `amount` is priced live against `amountUsd` at request time and moves with the rate; only `amountUsd` is fixed, at the demo product's price. The response carries `X-Payment-Chains`, `X-Payment-Address`, `X-Payment-Amount` and `X-Payment-Currency`. The caller sends the transfer, then retries the same URL with `X-Payment-TX` and `X-Payment-Chain`. The middleware verifies the transfer against the treasury it named. There is no session, no redirect and no browser in the loop. That is the rail an agent can drive.

`pay.one.ie` also answers a protocol dispatcher at `POST /`, carrying 66 protocols — read live from `protocolCount` on its discovery response. Five of them are the payment set: `x402_quote`, `x402_claim`, `payment_link_create`, `payment_link_quote`, `payment_link_claim`.

## Which chains are advertised

Two predicates gate every buyer-facing chain list, and they answer different questions. `isChainAdvertised` (`pay/backend/src/x402.ts`) asks whether this deployment can verify the rail at all. `platformTreasuryQuotable` (`pay/backend/src/config.ts`) asks whether the platform treasury on that rail has demonstrable custody. `/status` runs its own claim through both, so it cannot advertise a rail the platform-treasury quote doors (`GET /quote`, `POST /claim`, `x402_quote`) refuse. The x402 middleware's own challenge list passes only the first gate, over a shorter base list, so those two lists agree today rather than by construction.

Probed 2026-09-06:

| Call | Result |
|---|---|
| `GET /status` → `supportedPaymentChains` | `ETH`, `BASE`, `SUI` |
| `GET /x402/demo` → `X-Payment-Chains` | `ETH`, `BASE`, `SUI` |
| `GET /quote` prices | `ETH`, `BASE`, `ARB`, `OPT`, `SUI`, `ASI` |
| `GET /quote?chain=SOL`, `BTC`, `USDT` | `400 chain_not_quotable` |
| `GET /quote?chain=LIGHTNING` | `400 Lightning rail not configured` |

`SOL_ADVERTISED` is `false` in code: Solana's public RPCs are unreachable from Cloudflare Workers egress, and the verifier reads lamport deltas rather than SPL token balances. `BTC_TREASURY_PROVEN` is `false` because the platform's `TREASURY_BTC` is byte-identical to a placeholder address printed in the repo's own key docs. Both are code constants rather than environment variables, so re-enabling either is a reviewed change rather than a deployment setting. Neither gate touches seller-custodial Bitcoin on a payment link, where the address is the seller's own.

`ARB`, `OPT` and `ASI` quote but do not appear in an x402 challenge. The two lists pass through different predicates and have not been reconciled.

## Escrow, and who holds the key

`pay.one.ie` runs an escrow rail: `POST /escrow/create` mints a per-payment address, `GET /escrow/:paymentId/status` polls that address's on-chain balance, and `POST /escrow/:paymentId/forward` sweeps to the treasury pinned at create time. Later calls read that destination back off the record instead of trusting the caller.

Two facts about it matter more than the shape.

**It is not a developer-callable API.** Probed 2026-09-06, `POST /escrow/create` with no bearer returns `401 UNAUTHORIZED`. The token is `SERVER_SECRET`, shared byte-for-byte between `one.ie/web` and `pay/` and set only by `wrangler secret put`.

**Custody is the worker's.** `ESCROW_CUSTODY` is `'worker-derived'` (`pay/backend/src/config.ts`). Every escrow keypair is derived from `ESCROW_MASTER_SEED` and the worker signs the sweep itself. Keys are stripped before persistence but re-derivable at will, so one secret carries spend authority over every escrow address ever issued, historical ones included. Funds sitting in escrow are custodial. [Wallets and custody](/docs/wallets) covers the paths where they are not.

No payment was settled while writing this page. Nothing here asserts that a given rail completes end to end — only what each door advertises, and what it refuses.

## Where a 402 comes from

Four conditions produce a `402`, and they are not interchangeable.

| Source | Body | Means |
|---|---|---|
| `creditGate` | `insufficient_credits` | No ancestor in the funding walk covers the cost |
| `checkEntitlement` | `entitlement_exceeded` | A plan's hard limit for that feature |
| x402 middleware | `payment_required` | The route is gated and no valid transfer was presented |
| `debitCycle` | `build_credit_exhausted` | The build meter's hard limit, debited before the run spawns |

`enforceEntitlement` (`one.ie/web/src/middleware/entitlement.ts`) runs the credit check first: an empty funding chain is a 402 regardless of plan.

One case deliberately does not become a 402. A slug with no row in `owners` reads exactly like a broke tenant — zero grants, zero burns, no parent to walk — so `channels/src/credit-gate.ts` raises `UnknownWorkspaceError` instead. That is a routing bug, not a billing state, and reporting it as insufficient credits once sent a fully funded tenant to a top-up page.

A receiver call is not billed per call. `POST /api/ask/:receiver` carries no credit gate of its own; the meters sit on the work that costs money — agent turns, skill runs, builds.

## Known limits

- ONE's own credits top-up runs in Stripe test mode. `STRIPE_MODE = "test"` at `one.ie/web/wrangler.toml:110`. The mode is **per page**: one worker holds both a test and a live Stripe pair, and `STRIPE_LIVE_PAGES` (empty by default) names the exact page paths permitted to charge a real card — `resolveStripeForPage()` in `one.ie/web/src/lib/stripe-mode.ts` hands a listed page the live pair and every other page the test pair. Membership is exact and there are no wildcards; a request may NAME a page, it can never assert a mode. A listed page whose live pair is missing or mismatched refuses with a 503 rather than quietly minting a test session under copy that promises a real charge. `.claude/scripts/ad-copy-lint.sh --page <path>` asks the same question of the copy. `one.ie/web/src/lib/stripe-mode.ts` resolves that declaration against the secret key and reports a refusal when they disagree; the checkout route (`one.ie/web/src/pages/api/storefront/checkout.ts`) turns the refusal into a 503 rather than minting. The buy button's copy is derived from the publishable key, so it corrects itself in both directions. A seller's Connect checkout runs on the same platform secret key — what differs is the connected account it mints onto and the application fee, not the credentials.
- Escrow requires the platform's `SERVER_SECRET` and is custodial. There is no public escrow API today.
- `SOL` and `BTC` are refused by the platform treasury gate, for the two distinct reasons above.
- `ARB`, `OPT` and `ASI` quote but are not offered in an x402 challenge.
- The Lightning rail answers `Lightning rail not configured` on this deployment.
- The funding walk is eventually consistent. A concurrent burn between the gate's read and the caller's debit can take a balance below zero by one in-flight signal, accepted to keep the path lock-free.

## Next

- [Wallets and custody](/docs/wallets) — who holds the keys on each rail
- [Errors and limits](/docs/errors) — the full status-code contract
- [API](/docs/api) — the receiver door and its outcomes
- [Workflows](/docs/workflows) — the `sell` step that suspends a run until a buyer pays