# Support, Entitlements, Billing, and the Aje Entitlement Bridge

This page specifies how V1 supports customers, gates capabilities behind tiers,
and takes money — with crypto as the V1-primary rail. It serves three audiences:
support operators working cases and refunds, the entitlement and billing
engineers who wire payment settlement to product access, and the security
reviewers who need to know exactly which crypto-settlement claims are
**implemented** versus **provider-gated / aspirational**. It sits downstream of
the [Aje payment substrate](./substrate-aje.md) and upstream of the entitlement
gate enforced at the BFF; the connective tissue is the `@oshun/payments-bridge`
library and the `@oshun/billing-support` entitlement linkage.

Backlog: [`V1/TODOS.md` § 23](../TODOS.md). The crypto-settlement work is
tracked at § 23.1; the hub is [../ARCHITECTURE.md](../ARCHITECTURE.md).

> **Candor up front.** **Real and exercised:** the bridge's deterministic core
> (asset enum, confirmation policy, trust-tier disclosure gate, Ed25519 receipt
> signer, QR encoder, admin and security surfaces), the five new Aje chain
> modules with chain-valid address derivation and regtest/testnet e2e tests, and
> — since 2026-07-04 — the cross-rail entitlement-event parity with the fiat
> rail (see [Fiat Payments](./fiat-payments-and-wallets.md)). **Not wired:** the
> bridge declares `@aje/*` and `@oshun/event-bus` as dependencies but its `src/`
> imports **none** of them — it reimplements the model rather than adapting Aje
> (the ERC-20 paywall contract registry mirrors, with matching addresses, rather
> than imports `@aje/payments/stablecoins`). And **live wallet settlement is
> unexercisable headless** — it depends on a documented external merchant
> provisioner (BTCPay / OpenNode) that is not injected in-repo, so the BFF
> crypto-quote route is deliberately fail-closed. The `sign-up-and-pay-crypto`
> walkthrough is therefore graded **"pass (surfaces) / partial (settlement)"**.
> Where this page describes a path that the code does not yet contain, it says
> so.

---

## Support and customer operations

- **Support case substrate.** Cases use the `SupportCase` contract
  (`V1/TODOS.md` § 1.2) and route through the admin web surface. SLA is
  monitored with the `libs/shared/queue/src/sla-monitor.ts` patterns.
- **Customer operations.** The billing entry point lives in profile/settings.
  Subscription history, renewal, cancellation, restore, premium-persona access,
  and refund flows route through admin support. The crypto-specific admin
  surface (explorer links, invoice timeline, node-health panel, refund
  initiation) is described in
  [Admin billing surface](#admin-billing-surface-for-crypto--srcadmin-surface)
  below.

---

## Entitlements

Per-tier capability gates are evaluated at the BFF. The domain registry's
`DomainAuthPolicy.scopes` interlocks with entitlements, and premium personas,
voices, avatars, and generated media are gated through the Isis release-gate
model (see [Isis — Generation Control Substrate](./substrate-isis.md) and
[Persona, Avatar, and Voice Packs](./persona-avatar-voice-packs.md)).

### The concrete entitlement linkage — `@oshun/billing-support`

The architecture historically described entitlement granting only abstractly.
The real mechanism is `libs/oshun/billing-support/src/billing-aje-bridge.ts`,
which the library's own header documents as wiring a previously-islanded lib
("zero importers, no Aje reference, a second entitlement vocabulary") to the
canonical product tier. Two things happen there:

**1. Class → tier collapse.** Six billing `EntitlementClass` values collapse
onto three canonical `OshunEntitlementTier` values (from `@oshun/auth-client`),
so the product reads **one** tier vocabulary instead of two parallel models. The
exact map (`TIER_BY_CLASS`):

| `EntitlementClass` | `OshunEntitlementTier` |
| ------------------ | ---------------------- |
| `free`             | `free`                 |
| `starter`          | `pro`                  |
| `plus`             | `pro`                  |
| `pro`              | `premium`              |
| `scholar`          | `premium`              |
| `institutional`    | `premium`              |

**2. Settlement → state machine.** `applyPaymentSettlementToSubscription`
advances the billing subscription state machine from a normalized
`AjePaymentSettlement`
(`{ status: 'confirmed' | 'failed' | 'refunded'; reference: string }`). The
state set (`SUBSCRIPTION_STATES`) is `trial`, `active`, `past-due`, `grace`,
`paused`, `canceled`, `lapsed`, `restored`, and only the _entitling_ states —
`ENTITLING_STATES = {trial, active, grace, restored}` — actually grant the
class. A subscription in any non-entitling state falls back to `free` so a
lapsed payment immediately de-entitles rather than stranding a stale tier. The
settlement-driven transitions (`nextStateForSettlement`):

| Settlement `status` | From state                                             | To state   |
| ------------------- | ------------------------------------------------------ | ---------- |
| `confirmed`         | `trial` / `past-due` / `grace` / `paused` / `restored` | `active`   |
| `confirmed`         | `canceled` / `lapsed`                                  | `restored` |
| `confirmed`         | `active`                                               | no-op      |
| `failed`            | `active`                                               | `past-due` |
| `failed`            | (any other)                                            | no-op      |
| `refunded`          | `active` / `paused` / `trial` / `restored`             | `canceled` |

Every target is validated again by `transitionSubscription` against the declared
`VALID_TRANSITIONS` table, so an illegal move is rejected rather than applied; a
settlement with no transition for the current state is a `changed: false` no-op.
This is the bridge between "money confirmed on chain" and "product access
granted," and it is real, deterministic code.

---

## Billing — rail-agnostic, crypto-primary

Billing is payment-rail-agnostic. The V1 primary rail is non-custodial crypto
via the **Aje** domain (`libs/aje/`, the existing library-only Web3
infrastructure, named for the Yoruba orisha of wealth and commerce) plus the V1
entitlement bridge at `libs/oshun/payments-bridge/`. The V1.x optional fiat rail
uses Stripe-class providers via
`libs/shared/inbound-integrations/src/payment.ts`.

Both rails publish to the **same** entitlement event topics, so billing-action
reversal, entitlement suspension, trial-expiry behavior, conversion prompts, and
dunning logic are identical across rails. This was historically a design goal
only; it is now wired fact: the V1 fiat rail (`@oshun/fiat-payments` — Stripe
Billing plus Apple Pay / Google Pay, see
[Fiat Payments — Stripe Billing, Apple Pay, and Google Pay](./fiat-payments-and-wallets.md))
emits the bridge's `PaymentBusEvent` topics on `rail: 'fiat-stripe'` through a
`FiatEntitlementEmitter` that mirrors `CryptoEntitlementEmitter`
field-for-field, and a cross-rail test asserts structural equivalence per topic.
The older `payment.ts` connector-capability model in
`libs/shared/inbound-integrations` (whose `PaymentStatus` is
`requires_action | authorized | captured | refunded | failed`) remains a generic
multi-tenant connector framework and still does not emit bus topics itself — the
V1 fiat rail is `@oshun/fiat-payments`, not `payment.ts`.

---

## Crypto payment substrate — Aje and the V1 entitlement bridge

V1 promotes Aje into the V1-critical-path set (it had previously been
out-of-scope). V1 contributes two things on top of what Aje already ships:

1. **Five new chain modules in Aje** for chains the existing `@aje/chains` and
   `@aje/bitcoin` don't cover: Monero, Litecoin, TON, Ergo, and Tron.
2. **An Oshun-side bridge** at `libs/oshun/payments-bridge/`
   (`@oshun/payments-bridge`, v0.1.0, private) that owns the V1-specific
   concerns: the asset enum, per-tier confirmation policy, trust-tier disclosure
   gate, Ed25519 receipt signing, oracle aggregation, the cold-spend refund
   queue, customer/admin surfaces, and security gates.

> **The bridge is functionally standalone.** Its `package.json` declares
> `@aje/chains`, `@aje/oracles`, `@aje/payments`, `@aje/wallets`,
> `@oshun/audit-platform`, `@oshun/event-bus`, and `@oshun/identity` as
> dependencies, plus `@noble/curves` and `@noble/hashes`. But
> `grep "from '@aje" src/` returns **nothing**, and so does
> `grep "@oshun/event-bus" src/`. The bridge reimplements its own
> `AjeInvoiceStatus` vocabulary and never calls Aje. The "Aje stays
> library-only, consumed _through_ this bridge" framing describes a consumption
> path the code does not yet contain. The real consumer that wires the bridge is
> the **BFF** (see [BFF integration](#bff-integration--the-real-consumer)).

### What Aje already provides

| Aje library                 | What V1 reuses without change                                                                                                                                                                                                                                                                                              |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@aje/core`                 | Address types, hex utilities, audited crypto primitives (`@noble`/`@scure`).                                                                                                                                                                                                                                               |
| `@aje/chains`               | EVM L1/L2 and other account chains. The actual `chains/src/` tree contains `abstraction/`, `arbitrum/`, `avalanche/`, `cardano/`, `ethereum/`, `optimism/`, `polygon/`, `solana/`, `zksync/`. The `abstraction/` submodule provides `UnifiedProvider`, `ChainRegistry`, `NonceTracker` for chain-agnostic invoice polling. |
| `@aje/bitcoin`              | Lightning (LND/CLN/Phoenixd), LSP, BitVM, sBTC, Stacks, Ordinals, Runes, RGB.                                                                                                                                                                                                                                              |
| `@aje/wallets`              | HD wallet (BIP32/BIP39/BIP44), hardware wallet (Trezor/Ledger/Coldcard), MPC, key management, account abstraction, paymaster, session keys, social login, WalletConnect.                                                                                                                                                   |
| `@aje/payments/merchant`    | `Invoice`, `PaymentRequest`, `InvoiceLineItem`, `PaymentConfirmation`, `Refund`, `RecurringPayment`, `Subscription`, payment links, QR codes, escrow, milestones, notifications, receipts.                                                                                                                                 |
| `@aje/payments/stablecoins` | USDC, USDT, DAI, FRAX, GHO with risk + swap + aggregation.                                                                                                                                                                                                                                                                 |
| `@aje/payments/streaming`   | Payroll, flows, grants, batch (used by V1 metered billing top-ups).                                                                                                                                                                                                                                                        |
| `@aje/payments/circle`      | Circle Mint and Cross-Chain Transfer Protocol (CCTP) for USDC.                                                                                                                                                                                                                                                             |
| `@aje/payments/fiat-ramps`  | On/off-ramp integrations (V1.x).                                                                                                                                                                                                                                                                                           |
| `@aje/nodes`                | Full-node runner abstractions, light client, validator, RPC client primitives, Cardano node, Avalanche node.                                                                                                                                                                                                               |
| `@aje/privacy`              | Privacy pools and advanced-privacy primitives.                                                                                                                                                                                                                                                                             |
| `@aje/oracles`              | Pyth, Chainlink, RedStone, API3 oracle adapters.                                                                                                                                                                                                                                                                           |
| `@aje/settlement-escrow`    | Escrow primitives reused by V1 refundable-flow invoices.                                                                                                                                                                                                                                                                   |

> **Staleness note (corrected here).** Earlier copies of this table named
> Avalanche and zkSync as separate top-level `@aje/chains/<chain>/` modules.
> They are **not** top-level chain dirs; they live as `chains/src/avalanche/`
> and `chains/src/zksync/`. The five V1-added chains (below) _are_ top-level
> dirs under `libs/aje/chains/`.

### Five new chain modules added to Aje for V1

These five exist as real, separate packages under `libs/aje/chains/`, each with
its own `provider.ts`, node/RPC client, `types.ts`, and an e2e test against a
real testnet/regtest/stagenet/sandbox. The cryptographically-sensitive work runs
in the upstream node process; the TypeScript module owns the RPC client and the
**watch-only** address derivation.

| Module                      | What it derives, indexes, and confirms                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `libs/aje/chains/monero/`   | `monerod` JSON-RPC + `monero-wallet-rpc` view-only client (spend key never reaches TypeScript); per-invoice subaddress allocator (`subaddress-allocator/`); 10-block unlock policy; `get_tx_proof`-backed payment proofs (`payment-proof.ts`); subaddress-hash telemetry only; `stagenet-e2e.test.ts`.                                                                                                                                                                                                                                     |
| `libs/aje/chains/litecoin/` | Litecoin Core JSON-RPC; **BIP84 native SegWit** (`LTC_BIP84_DERIVATION_PREFIX = m/84'/2'/0'`, P2WPKH), watch-only xpub enforced at the correct depth; per-invoice receiving addresses at `…/0/<i>`; 1 / 3 / 6 confirmation tiers; `rbf-detector.ts` for fee-bump detection; `regtest-e2e.test.ts`.                                                                                                                                                                                                                                         |
| `libs/aje/chains/ton/`      | `v4r2` per-invoice subwallet derivation (`subwallet.ts`): a TON address is `workchain:sha256(StateInit cell)` where the v4r2 StateInit data is `seqno(u32) ‖ subwallet_id(u32) ‖ public_key(256) ‖ plugins`; per-invoice `subwallet_id` offsets from the v4 default (`698983191`) via a monotonic counter; `Cell.fromBoc` (`@ton/core`) verifies the BoC CRC32C; jetton (USDT-TON) `Transfer` indexing; Telegram @wallet deep links. **Trust-tier C.** `sandbox-e2e.test.ts`.                                                              |
| `libs/aje/chains/ergo/`     | `ergo-node` + `ergo-wallet-api` watch-only client; **BIP44** P2PK allocator (`p2pk-allocator.ts`: `ERG_BIP44_PURPOSE=44`, `ERG_SLIP44_COIN_TYPE=429`, `ERG_BIP44_DERIVATION_PREFIX = m/44'/429'/0'/0`) deriving real secp256k1 P2PK addresses via `@scure/bip32` `HDKey`; UTXO-set polling; 5 / 10 / 30 confirmation tiers; `testnet-e2e.test.ts`.                                                                                                                                                                                         |
| `libs/aje/chains/tron/`     | Real base58check address derivation (`tron-address.ts`): `keccak_256` of the secp256k1 uncompressed pubkey `X‖Y`, last 20 bytes, `0x41` network prefix, double-SHA-256 checksum. TRC-20 `Transfer` indexing scoped to **USDT-TRC20 only** (`USDT_TRC20_CONTRACT='TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'`, Nile testnet `TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj`, `TRON_USDT_DECIMALS=6`); native TRX **not accepted** in V1; `TRON_CONFIRMATION_DEPTH=19`. **Trust-tier C**; invoice creation rejects unacknowledged. `nile-testnet-e2e.test.ts`. |

---

## V1 entitlement bridge — `libs/oshun/payments-bridge/`

The bridge's real `src/` tree is larger than the legacy "thin and Oshun-aware"
table implied. The directory layout is the ground truth:

```
src/
  state-mapper.ts          trust-tier-disclosure.ts   (flat files)
  oracle-aggregator/  cold-spend-queue/  receipt-signer/  entitlement-bus/
  customer-surface/   admin-surface/     security-gates/
  index.ts
```

> **Module-name corrections.** There is **no** `webhook-router.ts`, **no**
> `crisis-suppression.ts`, and **no** `telegram-handoff.ts` in this package. And
> `trust-tier-disclosure` is a **flat file**, not a directory, while
> `oracle-aggregator`, `cold-spend-queue`, and `receipt-signer` are
> **directories**, not flat files. The real homes are: event emission →
> `entitlement-bus/`, crisis suppression →
> `customer-surface/telegram-bot-router.ts` (gates only the Telegram `/upgrade`
> command, not "every invoice-creation path"; a separate, unrelated
> crisis-suppression lives in the BFF at `apps/oshun/bff/src/safety/`),
> Telegram/QR rendering →
> `customer-surface/{telegram-bot-router,qr-matrix,qr-svg,paywall-spec}.ts`.

### State mapper — `src/state-mapper.ts`

The state mapper is the heart of the bridge and is **fully self-contained**: it
defines its own status vocabulary rather than importing Aje's. Its exported
enums:

- **`V1_PAYMENT_ASSETS`** — exactly **32** assets: the natives `btc-onchain`,
  `btc-lightning`, `ltc`, `xmr`, `eth-mainnet`, `eth-base`, `eth-arbitrum`,
  `eth-optimism`, `matic-polygon`, `sol`, `ton`, `ada`, `erg`; then
  `usdc-{mainnet,base,arbitrum,optimism,polygon,solana}`,
  `usdt-{mainnet,base,arbitrum,optimism,polygon,tron,solana,ton}`, and
  `dai-{mainnet,base,arbitrum,optimism,polygon}`.
- **`AJE_INVOICE_STATUSES`** — `draft`, `pending`, `paid`, `partial`,
  `refunded`, `expired`, `cancelled`. This is the bridge's **own** vocabulary,
  not Aje's. (Aje's real `@aje/payments/merchant` `InvoiceStatus` is
  `draft | sent | viewed | partial | paid | overdue | cancelled | refunded` and
  `ConfirmationStatus` is
  `pending | confirming | confirmed | failed | finalized`.) But the bridge
  **never imports them**, so its enum drops `sent`/`viewed`/`overdue` and adds
  `pending`/`expired`.
- **`AJE_CONFIRMATION_STATUSES`** — `unconfirmed`, `confirmed`, `finalized`
  (again, not Aje's five-value confirmation enum).
- **`V1_PAYMENT_EVENT_TYPES`** — `payment.invoice.settled`,
  `payment.invoice.underpaid`, `payment.invoice.expired`,
  `payment.refund.broadcast`.

Functions: `mapAjeStateToV1`, `mapAjeStateToV1WithAudit`,
`requiredConfirmations`, `toAuditRecord`. The mapping rule is:

| Aje status              | Emitted event                  | Grants entitlement?               |
| ----------------------- | ------------------------------ | --------------------------------- |
| `paid` (depth met)      | `payment.invoice.settled`      | yes, iff `entitlementId !== null` |
| `paid` (depth not met)  | _(null — wait for more confs)_ | —                                 |
| `partial`               | `payment.invoice.underpaid`    | no (carries `underpaidByAtomic`)  |
| `refunded`              | `payment.refund.broadcast`     | no                                |
| `expired` / `cancelled` | `payment.invoice.expired`      | no                                |
| `draft` / `pending`     | _(null)_                       | —                                 |

`mapAjeStateToV1WithAudit` wraps the same logic and appends an immutable
`V1PaymentStateAuditRecord` (`kind: 'payment-state-transition'`,
`immutable: true`) through an injected `V1PaymentStateAuditAppender` before
returning.

> **Vocabulary reconciliation (2026-07-04).** The state mapper's emitted types
> and the entitlement-bus topics are now one vocabulary:
> `entitlement-bus/topics.ts` defines `payment.invoice.confirmed`,
> `payment.invoice.settled`, `payment.invoice.underpaid`,
> `payment.invoice.expired`, and `payment.refund.broadcast`, and
> `CryptoEntitlementEmitter` gained `emitUnderpaid` / `emitExpired` (tests
> assert the topic strings equal the mapper's emitted types exactly). The admin
> invoice timeline also gained the previously-unrepresentable `underpaid` state
> with legal-predecessor validation. Still true and worth knowing: the "5-stage
> lifecycle" with `seen` / `overpaid` stages described in older companion docs
> remains aspirational — `state-mapper.ts` models no `seen` state and no
> `overpaid` terminal (an overpaid invoice settles; the paid-branch now enforces
> an underpaid-deficit invariant instead of trusting Aje's `paid` status
> blindly).

### Confirmation policy — `CONFIRMATION_POLICY`

A per-asset, per-amount-tier minimum confirmation depth. Amount tiers are
`micro` / `standard` / `high-value`. A `paid` invoice only settles once
`observedConfirmations >= required` **and** the confirmation status is
`confirmed` or `finalized`. Representative depths:

| Asset           | micro | standard | high-value | Notes                                |
| --------------- | ----- | -------- | ---------- | ------------------------------------ |
| `btc-onchain`   | 0     | 1        | 3          | 0-conf accepted for micro            |
| `btc-lightning` | 0     | 0        | 0          | instant off-chain                    |
| `ltc`           | 1     | 3        | 6          |                                      |
| `xmr`           | 10    | 10       | 20         | 10-block Monero unlock               |
| `eth-mainnet`   | 12    | 12       | 30         |                                      |
| `ada`           | 15    | 15       | 30         |                                      |
| `erg`           | 5     | 10       | 30         |                                      |
| `usdt-tron`     | 19    | 19       | 19         | matches `TRON_CONFIRMATION_DEPTH`    |
| `matic-polygon` | 256   | 256      | 256        | Heimdall checkpoint window           |
| `sol`           | -1    | -1       | -2         | commitment **sentinels**, not depths |

Solana has no integer block depth, so the policy encodes RPC commitment levels
as sentinels: `SOLANA_COMMITMENT_CONFIRMED = -1` and
`SOLANA_COMMITMENT_FINALIZED = -2`. `computeConfirmationMeetsRequirement` reads
the sentinel and checks the confirmation _status_ string instead of an integer
count — `-1` accepts `confirmed` or `finalized`, `-2` requires `finalized`. The
Base/Arbitrum/Optimism EVM L2s carry tall constant depths (150 / 50 / 120) that
reflect each rollup's reorg surface; these apply identically to their
USDC/USDT/DAI variants.

### Trust-tier disclosure — `src/trust-tier-disclosure.ts`

A single flat file holding the `RAIL_REGISTRY` (a `RailDescriptor` per asset)
and the invoice-creation disclosure gate. Each descriptor carries a `tier`
(`RAIL_TIERS = ['A','B','C']`), an `issuerTrustClass`
(`ISSUER_TRUST_CLASSES = ['native','decentralized-issuer','central-issuer-with-freeze']`),
an English `disclosureCopyEn`, and `requiresDisclosureAcknowledgement`.

| Tier  | Meaning                                                   | Example assets                                                                       | Ack required? |
| ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------- |
| **A** | Native UTXO / native account chain, no central freeze     | `btc-onchain`, `btc-lightning`, `ltc`, `xmr`, `eth-mainnet`, `ada`, `erg`            | no            |
| **B** | Rollup-secured L2 (sequencer can pause) or central-issuer | `sol`, `eth-{base,arbitrum,optimism}`, `matic-polygon`, all USDC/USDT/DAI on B-rails | yes           |
| **C** | Single-validator-set chain (TON) or Tron central-issuer   | `ton`, `usdt-tron`, `usdt-ton`                                                       | yes           |

`sol` is **B** despite being native (validator concentration + outage history);
`ton` is **C** (TON Foundation governance). All USDC/USDT are
`central-issuer-with-freeze`; DAI is `decentralized-issuer` (MakerDAO, but its
collateral basket includes freezable USDC). Every Tier-B / Tier-C rail and every
centrally-issued asset sets `requiresDisclosureAcknowledgement: true` and ships
a plain-English warning that Lilith policy reviews each release (see
[Lilith — Contemplative Policy Substrate](./substrate-lilith.md)).

The gate is `gateInvoiceCreation(request)`. For a rail that requires
acknowledgement, if `disclosureAcknowledgedAtUnixSeconds` is `null` or `<= 0` it
returns:

```json
{
  "verdict": "block",
  "reason": "disclosure-acknowledgement-required",
  "disclosureCopy": "USDT on Tron is issued by Tether and can be frozen at the issuer's discretion. The Tron network is operated by a small set of Super Representatives."
}
```

Otherwise it returns `{ "verdict": "allow" }`. The address is never rendered for
a blocked invoice. `listSupportedAssets()` (consumed by the BFF, see below)
returns `V1_PAYMENT_ASSETS` in deterministic order.

### Oracle aggregator — `src/oracle-aggregator/`

`price-feed.ts` composes a fiat-rate median across **Kraken REST**, **CoinGecko
REST**, and an on-chain **Uniswap v3 30-minute TWAP** (the TWAP window mitigates
flash-loan manipulation), alongside the `@aje/oracles` adapters (Chainlink /
Pyth / RedStone). Quotes are validated before medianing — non-finite or
non-positive rates are dropped (a NaN-parsing feed can no longer sail through
the divergence gate), and fewer than two valid sources yields a
`rejected-insufficient-sources` verdict that carries **no** canonical rate. The
median is the canonical rate; the pair-wise spread `(max − min) / median` is
captured per invoice for monitoring. `tor-egress.ts` routes the off-Aje HTTP
sources through a SOCKS proxy so price queries don't leak server egress;
`rate-lock.ts` is the per-invoice rate-lock cache so a customer's quoted rate is
frozen for the invoice lifetime.

### Cold-spend refund queue — `src/cold-spend-queue/`

Every refund or sweep that needs an offline signing step lands on this queue.
`types.ts` enumerates the chain families (`COLD_SPEND_CHAIN_FAMILIES`) and their
unsigned-tx formats (`COLD_SPEND_TX_FORMATS`): `psbt-v2`, `eip1559-rlp`,
`xmr-unsigned`, `solana-versioned`, `ton-external-message`, `cardano-cbor`,
`ergo-unsigned`, `tron-raw`. The queue (`queue.ts`) carries entries through a
`ColdSpendStatus` lifecycle; `sweep-policy.ts` consolidates receipts above the
sweep threshold to a **2-of-3 multisig vault per chain family** (BTC/LTC P2WSH,
EVM Safe, Monero MMS, Solana SPL/Squads, TON multisig, Cardano Plutus-script,
Ergo P2S/Sigma); `hw-signing-fixture.ts` models the hardware co-signer; and
`audit-attestation.ts` emits an audit-platform attestation per signing event.

> The vault topology, air-gapped signing station, and hardware co-signers are
> **policy/spec described in code (types + comments) and tested with fixtures**,
> not provable as a running operational deployment in-repo. See
> [Operational invariants](#operational-invariants).

### Receipt signer — `src/receipt-signer/`

A customer holding only the receipt and access to the chain can verify Oshun's
claim without trusting Oshun's API. `receipt-signer.ts` signs the
`ReceiptPayload` with **Ed25519** (`@noble/curves/ed25519`) over a **canonical
JSON** encoding: keys sorted, `bigint`s serialized as decimal strings (so
amounts survive JSON). `verifyReceipt` re-derives the canonical bytes and checks
the signature against the audit-platform public key. The `ReceiptPayload`
(`types.ts`) carries `invoiceId`, `merchantId`, `customerLocale`, `txId`,
`blockHash`, `blockHeight`, `asset` (one of `RECEIPT_ASSETS`, **17** entries),
`amount` (smallest-unit `bigint`), `fiatAmountMinor` (cents `bigint`),
`confirmedAtUnixSeconds`, a `tax: TaxBreakdown`, a `verificationSnippet`, and an
optional `moneroPaymentProof`.

> **Receipt assets are now chain-qualified (2026-07-04).** `RECEIPT_ASSETS` was
> previously 17 chain-ambiguous symbols; it is now the full 32-member
> `V1PaymentAsset` vocabulary with a compiler-enforced total mapping
> (`RECEIPT_ASSET_FOR_V1_ASSET`) and a separate display-symbol table, so a
> USDC-on-Base payment receipts as `usdc-base`, and `verification-snippet.ts`
> names per-chain RPC placeholders (`<base-rpc-endpoint>` etc.) instead of a
> generic endpoint that pointed L2 customers at the wrong chain. Fiat tax
> rendering (`locale-tax.ts`) is currency-exponent-aware (JPY/KRW 0-decimal,
> KWD/BHD 3-decimal) and bigint-exact.

> **Monero proof is a 2-tuple, not a 3-tuple.** `MoneroPaymentProof` is exactly
> `{ txKey, address }` (from `get_tx_proof`). `txId` is a **separate top-level**
> `ReceiptPayload` field, not part of the proof tuple.

Two customer-facing extras are real and worth naming: `verification-snippet.ts`
(`buildVerificationSnippet`) produces a curl/shell command a customer can run to
reproduce the on-chain check, and `locale-tax.ts` reuses the fiat receipt's
`TaxBreakdown` formatting (`subtotalMinor`, `taxMinor`, `totalMinor`, `taxRate`,
`taxJurisdiction`) so crypto receipts carry the same tax disclosure as fiat
ones.

### Entitlement bus — crypto/fiat parity _by design_

`entitlement-bus/topics.ts` defines three topics — `payment.invoice.confirmed`,
`payment.invoice.settled`, `payment.refund.broadcast` — whose event shapes are
_structurally identical_ across rails so the entitlement service need not know
whether a payment came from Stripe/Adyen/PayPal or from a crypto rail. The
`rail` field on the envelope (`PaymentRail`) distinguishes them; events carry
`schemaVersion: 1`. `emitter.ts` exposes `CryptoEntitlementEmitter` with
`emitConfirmed` / `emitSettled` / `emitRefundBroadcast`; it has **no I/O of its
own** — a `publish` function is injected, and it never imports
`@oshun/event-bus`. `eventsStructurallyEquivalent` is the helper that asserts a
crypto event matches a fiat event of the same topic field-for-field (ignoring
`rail`).

> **`PaymentRail` is 14 crypto + 4 fiat.** (`btc-onchain`, `btc-lightning`,
> `ltc`, `evm-ethereum`, `evm-base`, `evm-arbitrum`, `evm-optimism`,
> `evm-polygon`, `xmr`, `sol`, `ton`, `cardano`, `ergo`, `tron`) plus 4 fiat
> (`fiat-stripe`, `fiat-adyen`, `fiat-paypal`, `fiat-generic`). The
> doc-comment's old "thirteen crypto rails" off-by-one was corrected. The
> `fiat-stripe` rail is now actually emitted by `@oshun/fiat-payments`.

### Customer surface — `src/customer-surface/`

- **`telegram-bot-router.ts`** — `routeUpgradeCryptoCommand` handles
  `/upgrade --crypto <asset>`. It applies crisis-state suppression **first**:
  when `crisisState === 'active'` it returns
  `{ action: 'suppress', reason: 'crisis-active' }` (no payment surface during a
  crisis-flagged conversation); otherwise it validates the asset against
  `PAYWALL_ASSETS` and returns a `render-paywall` directive with the resolved
  chains, or a `show-help`.
- **`paywall-spec.ts`** — `PaywallRenderable` construction is gate-aware: the
  public path is `buildPaywall(input)`, which runs the trust-tier disclosure
  gate first and returns either `{ blocked: true }` carrying only disclosure
  copy (no address, no QR — the "address is never rendered for a blocked
  invoice" invariant is now enforced by construction, not by caller discipline)
  or the renderable. The low-level per-target builders were renamed
  `buildUngated*` (`buildUngatedOnchainAddressPaywall`, Lightning BOLT11, Monero
  subaddress, Solana Pay URL, TON deep link, EVM EIP-681) and are documented as
  internal plumbing for DTO rehydration. Two long-standing URI bugs were fixed
  here: ERC-20 assets now render the EIP-681 **token** form
  `ethereum:<tokenContract>@<chainId>/transfer?address=…&uint256=…` (backed by
  an EIP-55-verified per-asset contract registry mirroring
  `@aje/payments/stablecoins`) instead of paying native wei, and Monero
  `tx_amount` renders decimal XMR instead of piconero.
- **QR encoder — `qr-matrix.ts` / `qr-svg.ts`.** A **from-scratch** ISO/IEC
  18004 QR Code Model 2 byte-mode encoder (smallest fitting version 1–40 at the
  requested error-correction level). Apps consume the boolean matrix directly:
  web renders one `<rect>` per module, mobile one `<View>` per module, Telegram
  emits an SVG attachment. This is a real V1 artifact, not a re-export of
  `@aje/payments/merchant`'s QR.
- **`reminder-cadence.ts`** — the renewal-window reminder engine. It is pure
  (computes due times, no I/O) and uses
  `REMINDER_OFFSETS_SECONDS = { sevenDays, twentyFourHours, oneHour }` — the **7
  d / 24 h / 1 h** cadence the companion docs describe.
- **`asset-chain-filter.ts`** — maps a paywall asset to the chains Oshun accepts
  it on; **`disclosure-gate.ts`** is the customer-side mirror of the trust-tier
  acknowledgement gate.

### Admin billing surface for crypto — `src/admin-surface/`

Entirely real and previously undocumented in the bridge table:
`explorer-urls.ts` (`DEFAULT_EXPLORERS` per chain + `txExplorerUrl` /
`addressExplorerUrl`), `invoice-timeline.ts`, `node-health-panel.ts`,
`refund-initiation.ts`, and `invariant-guards.ts`. This is the operator surface
behind admin support's refund and reconciliation flows.

### Security gates — `src/security-gates/`

Also real and previously only alluded to as "tests cover":
`build-time-invariants.ts` (grep-style assertions that **forbid** on-chain
customer identifiers: no `OP_RETURN` writes in BTC/LTC tx-build paths, no Monero
`tx_extra` payment-ID writes, no Solana memo-program customer IDs; plus
`assertMoneroWalletRpcIsViewOnly`), `chaos-tester.ts`, `disclosure-audit.ts`,
`node-health-probes.ts`, and `tabletop.ts`. These implement the build-time
invariant scanning, chaos/tabletop testing, and node-health probing that the
architecture had only gestured at.

---

## BFF integration — the real consumer

The architecture implied apps consume Aje "through the bridge" without naming
the layer that actually does it. That layer is the **BFF**
(`apps/oshun/bff/src/`):

- `routes/domain-stubs.ts` imports `listSupportedAssets` from
  `@oshun/payments-bridge` and serves the **real** crypto-asset catalog. The
  crypto-quote route is bound to a server-side **plan price book**
  (`payments/plan-price-book.ts`, env-overridable): the quoted amount is derived
  from the plan, a client-echoed mismatch is a 400, and settlement re-verifies
  paid ≥ book price before any entitlement grant (a below-book invoice settles
  the money but withholds the grant with an operator-visible support flag).
- `payments/payments-composition.ts` and `payments/quote-builder.ts` import
  `ReceiptSigner` and `V1PaymentAsset` from the bridge and wire a single
  `PaymentsRuntime`. The BFF `payments/` dir also holds `invoice-store.ts` (with
  terminal-invoice retention), `settlement-route.ts`, and
  `settlement-receipt.ts`.
- The settlement webhook verifies its HMAC over the **exact raw request bytes**
  with a signed timestamp (±300 s) — not a re-serialized body — and runs a
  settlement-reference replay guard. Every confirmation now really uses the
  runtime's Ed25519 `ReceiptSigner`: the route builds the bridge's
  `ReceiptPayload` (chain-qualified asset, verification snippet), signs it,
  persists it on the invoice, and returns it; without a bound signer it refuses
  (503) rather than confirming receipt-less.
- App-store IAP (Apple verify + **App Store Server Notifications V2** at
  `/v1/billing/app-store/apple/notifications`, Play purchases + RTDN) and the
  fiat rail both recompute `entitlementTierForSubscription` and persist it via
  `customerAuthStateStore.updatePlan` — the exact field the entitlement
  middleware reads — including downgrades to `free` on refund/revoke/expiry.
- The fiat rail registers at `payments/fiat-routes.ts` (`/v1/payments/fiat/*`):
  Stripe Billing checkout + webhook (raw-byte `Stripe-Signature` verification),
  the wallet server legs (Apple Pay merchant validation behind the SSRF
  allowlist + mTLS seam, Google Pay gateway config, Stripe-tokenized wallet
  attach), and the parity-event feed at `GET /v1/payments/fiat/events`. See
  [Fiat Payments](./fiat-payments-and-wallets.md).
- `@aje` appears in the BFF **only as comments** — there is no live Aje call.

The crypto-quote route is **fail-closed by design**. `server.ts` binds
`resolvePaymentsRuntime(process.env)`; in-repo no settlement provisioner is
injected, so the runtime is `null` and the quote route returns **503 with no
address issued**. The log line is explicit: _"payments: crypto quoting
fail-closed (no settlement provisioner) — quote route returns 503."_ The comment
at `server.ts:930` documents that a real deploy "constructs the provisioner
(BTCPay/OpenNode/@aje) + binds a real runtime here." This is an
[honest fail-loud seam](./trust-safety-and-privacy.md), not a stub: it refuses
to fabricate an address rather than pretend to settle.

---

## Operational invariants

Applied across Aje + the bridge (spec/policy described in code, fixture-tested,
not provable as a running deployment in-repo):

- The application server holds **view keys and watch-only xpubs only** for every
  chain. Spend keys live on an air-gapped signing station behind hardware-wallet
  co-signers (Trezor / Ledger / Coldcard). `@aje/wallets/hardware-wallet` is the
  only library allowed to touch a spend key.
- Sweeps consolidate to a **2-of-3 multisig vault per chain family** using each
  chain's native multisig primitive (see `cold-spend-queue/sweep-policy.ts`).
- **Per-invoice fresh derivation** on every chain (Litecoin `m/84'/2'/0'/0/<i>`,
  Ergo `m/44'/429'/0'/0/<i>`, Tron per-invoice address, TON per-invoice
  `subwallet_id`, Monero per-invoice subaddress). Address reuse is rejected.
- **Telemetry stores only the hash** of any per-invoice identifier (Monero
  subaddress, Solana reference pubkey, TON subwallet contract address).
- **No on-chain customer identifiers** — enforced at the build layer by
  `security-gates/build-time-invariants.ts`.
- **Trust-tier disclosures are mandatory** on every Tier-B / Tier-C rail and
  centrally-issued asset; the acknowledgement timestamp is recorded before any
  address is rendered (`gateInvoiceCreation`). Disclosure copy is reviewed by
  Lilith policy each release.

---

## Honest gaps — implemented vs. aspirational

| Claim                                                                                                  | Reality                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "The bridge adapts Aje's merchant invoice contracts / depends on `@aje/*`."                            | **Aspirational.** `src/` imports **zero** `@aje/*` (and zero `@oshun/event-bus`). The bridge reimplements its own `AjeInvoiceStatus` vocabulary and never calls Aje. The `@aje/*` deps are declared but unused.                                                                                                                                                                                                             |
| "Both rails publish identical-schema entitlement events."                                              | **Wired (2026-07-04).** `@oshun/fiat-payments` emits the bridge's `PaymentBusEvent` topics on `fiat-stripe` via `FiatEntitlementEmitter`; a cross-rail test asserts per-topic structural equivalence and key-set equality. The generic `payment.ts` connector framework remains schema-separate but is not the V1 fiat rail.                                                                                                |
| "Sign up and pay with crypto, end to end."                                                             | **Pass (surfaces) / partial (settlement).** `/billing/crypto` and `/aaa-upgrade` render; `/v1/payments/*` and `/v1/entitlements/aaa` respond. **Live wallet settlement** needs a real merchant provisioner (BTCPay/OpenNode), a documented external dependency; `v1-completeness-audit-2026-06-22` lists `sign-up-and-pay-crypto` as **partial**. The BFF quote route is fail-closed (503) until a provisioner is injected. |
| "Self-hosted nodes, air-gapped signing, hardware co-signers, multisig vaults, Tor egress."             | **Spec/policy + fixtures**, not a verifiable running deployment. The code (`cold-spend-queue/`, `oracle-aggregator/tor-egress.ts`, `security-gates/`) describes and fixture-tests these; no running node or live settlement is verifiable in-repo.                                                                                                                                                                          |
| "Event lifecycle: created → seen → confirmed → settled → entitlement_granted, with overpaid terminal." | **Partially resolved.** Mapper/bus vocabularies are reconciled (`underpaid`/`expired` topics + emitter methods exist; the admin timeline models `underpaid` with legal-predecessor checks). Still aspirational: no `seen` state and no `overpaid` terminal in `state-mapper.ts` (overpayment settles; the paid branch now enforces an underpaid-deficit invariant).                                                         |

The honest framing: the **deterministic core is real and tested** (chain-valid
derivation with regtest/testnet e2e, confirmation policy, disclosure gate,
Ed25519 receipts, QR encoder, admin/security surfaces, and the
`@oshun/billing-support` entitlement state machine). What remains gated is the
**operational settlement plane** — a live merchant provisioner and the
node/signing infrastructure — which is correctly represented in-repo as a
fail-closed seam rather than faked success.

---

## Related

- [Fiat Payments — Stripe Billing, Apple Pay, and Google Pay](./fiat-payments-and-wallets.md)
- [Aje — Non-Custodial Payment Substrate](./substrate-aje.md)
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md)
- [Lilith — Contemplative Policy Substrate](./substrate-lilith.md)
- [Isis — Generation Control Substrate](./substrate-isis.md)
- [Customer-Facing Domains](./customer-domains.md)
- [Security, Privacy, and Compliance](./security-privacy-compliance.md)
- [Messaging Channels](./messaging-channels.md)
- [Subsystem Glossary](./glossary.md)
- Hub: [../ARCHITECTURE.md](../ARCHITECTURE.md)
