# Aje — Non-Custodial Payment Substrate

`Aje` is V1's blockchain and Web3 payment substrate — the library-only domain
(`libs/aje/`) that lets Oshun accept money without ever custodying customer
funds and without a centralized processor in the middle. Named for the Yoruba
orisha of wealth, commerce, and the marketplace, it serves the billing and
entitlement surfaces (a learner upgrades to a paid tier by paying in crypto)
through one Oshun-specific glue library, `libs/oshun/payments-bridge/`. This
page specifies what Aje ships, the five chains V1 contributes, and the bridge
that turns an on-chain confirmation into an Oshun entitlement — and it is candid
about where the wiring is real versus where it is spec-only or provider-gated.

This page sits alongside the other substrate pages
([Sophia — Grounding Substrate](./substrate-sophia.md),
[Isis — Generation Control Substrate](./substrate-isis.md),
[Lilith — Contemplative Policy Substrate](./substrate-lilith.md)) and is the
architectural counterpart to the product-facing
[Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md).
The backlog lives at §23.1; cross-cutting dependencies at deps§13. The hub is
[../ARCHITECTURE.md](../ARCHITECTURE.md).

## Why a non-custodial substrate

The product promise (see [V1 Product Promise](./product-promise.md)) is that a
learner can pay for an upgrade without Oshun touching their keys, holding their
balance, or routing them through a card processor whose chargeback and KYC
posture would distort the product. The whole design follows from that:

- **No custody.** The application server holds **view keys and watch-only xpubs
  only**. It can _watch_ a deposit address and _detect_ a payment, but it cannot
  move funds. Spend authority lives behind hardware co-signers on an air-gapped
  signing station. That separation is described in code comments and types (see
  [Cold-spend refund queue](#cold-spend-queue--refund-signing-path)), but it is
  an operational concern, not provable from the repo alone.
- **No centralized processor.** Confirmation is read directly from chain nodes
  and RPC providers; the bridge decides when "enough confirmations" have accrued
  for a given asset and amount tier, then emits an entitlement event.
- **Audited cryptography only.** Every address-derivation and signing path uses
  `@noble/curves`, `@noble/hashes`, and `@scure/*` — no hand-rolled crypto.
- **TypeScript end to end.** Cryptographically-sensitive node work (`monerod`,
  `monero-wallet-rpc`, `ergo-node`, `java-tron`) runs in upstream processes; the
  Aje TypeScript modules own only the JSON-RPC client and the deterministic
  key-derivation math.

### Real-vs-aspirational at a glance

Several pieces of this design are genuinely implemented and tested; several are
operational or external-integration concerns that the repo describes but cannot
exercise headless. Stating it plainly up front:

| Area                                                                                                             | Status                                                                                             |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Five new chain modules (chain-valid address derivation, RPC clients, confirmation policies, regtest/testnet e2e) | **Implemented, real, tested**                                                                      |
| `state-mapper.ts` confirmation policy + V1 event mapping                                                         | **Implemented, real, tested**                                                                      |
| `trust-tier-disclosure.ts` rail registry + acknowledgement gate                                                  | **Implemented, real, tested**                                                                      |
| Ed25519 `ReceiptSigner` + verification snippet                                                                   | **Implemented, real, tested**                                                                      |
| Oracle aggregator, cold-spend queue, entitlement bus, QR generation, admin/security surfaces                     | **Implemented, real, tested**                                                                      |
| BFF consumes the bridge (catalog, quotes, receipt signing, invoice store)                                        | **Implemented, real**                                                                              |
| The bridge _imports Aje_ (`@aje/chains`, `@aje/payments`, …)                                                     | **Not wired** — declared as deps, imported by zero source files                                    |
| Live wallet → merchant settlement                                                                                | **Provider-gated** — needs an external merchant integration (BTCPay/OpenNode); fail-closed in-repo |
| Self-hosted nodes, air-gapped signing, hardware co-signers, multisig vaults, Tor egress                          | **Spec / policy only** — described in types and comments, not provable in-repo                     |

## Aje library surface (reused without change)

V1 reuses Aje's existing libraries as a chain/wallet/payment toolkit. The
following table reflects what Aje ships; the **important caveat** is that the V1
bridge does **not** call any of it (see
[The bridge is functionally standalone](#the-bridge-is-functionally-standalone)).

| Aje library                                              | What it provides                                                                                                                                                                                                                                                                              |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@aje/core`                                              | Address types, hex utilities, audited crypto primitives (`@noble`/`@scure`).                                                                                                                                                                                                                  |
| `@aje/chains`                                            | EVM L1 + L2s and non-EVM chains. The `chains/src/` tree contains `ethereum/`, `arbitrum/`, `optimism/`, `polygon/`, `avalanche/`, `zksync/`, `cardano/`, `solana/`, plus the `abstraction/` submodule (`UnifiedProvider`, `ChainRegistry`, `NonceTracker`). Base is covered via the OP Stack. |
| `@aje/bitcoin`                                           | Lightning (LND/CLN/Phoenixd), LSP, BitVM, sBTC, Stacks, Ordinals, Runes, RGB.                                                                                                                                                                                                                 |
| `@aje/wallets`                                           | HD (BIP32/39/44), hardware (Trezor/Ledger/Coldcard), MPC, account abstraction, paymaster, session keys, WalletConnect.                                                                                                                                                                        |
| `@aje/payments/merchant`                                 | `PaymentRequest`, `Invoice`, `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/circle`                                   | Circle Mint + Cross-Chain Transfer Protocol (CCTP) for USDC.                                                                                                                                                                                                                                  |
| `@aje/oracles`                                           | Pyth, Chainlink, RedStone, API3 adapters.                                                                                                                                                                                                                                                     |
| `@aje/nodes` / `@aje/privacy` / `@aje/settlement-escrow` | Node runners, privacy pools, escrow primitives.                                                                                                                                                                                                                                               |

> **Staleness fix.** Earlier prose claimed `@aje/chains` "already provides
> Avalanche and zkSync" as if they were missing. They are present — but as
> `chains/src/avalanche/` and `chains/src/zksync/`, **not** as top-level
> `libs/aje/chains/<chain>/` modules like the five V1 additions below.

### Aje's _actual_ merchant vocabulary

The real Aje merchant types (`libs/aje/payments/src/merchant/types.ts`) define:

- `InvoiceStatus` =
  `draft | sent | viewed | partial | paid | overdue | cancelled | refunded`
- `ConfirmationStatus` = `pending | confirming | confirmed | failed | finalized`

These are accurate to Aje. The catch — detailed below — is that the V1 bridge
**reimplements its own status vocabulary** rather than importing these.

## The five V1 chain modules

V1's contribution to Aje is five new top-level chain modules at
`libs/aje/chains/{monero,litecoin,ton,ergo,tron}/`. Each is its own package with
a `provider.ts`, a node/RPC client, `types.ts`, and an end-to-end test against a
local regtest / testnet / stagenet / sandbox. Every one derives **chain-valid**
addresses with audited crypto. The audit that prompted these modules found that
the earlier per-invoice addresses were _synthetic_ — `sha256`-derived strings
that no node would validate and no operator could sweep.

| Module                      | Derivation + policy (real)                                                                                                                                                                                                                                                                                     | e2e harness      |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `libs/aje/chains/monero/`   | Per-invoice subaddress allocator (account `0`, fresh `subaddress_index`); view-only `monero-wallet-rpc`; 10-block unlock; `get_tx_proof` payment proofs.                                                                                                                                                       | stagenet e2e     |
| `libs/aje/chains/litecoin/` | BIP84 native-SegWit (P2WPKH) addresses at `m/84'/2'/0'/0/*` (`LTC_BIP84_DERIVATION_PREFIX = m/84'/2'/0'`, coin type 2 per SLIP-0044); watch-only xpub enforced at depth 3; `rbf-detector.ts` for fee-bump detection; 1/3/6 confirmation tiers.                                                                 | regtest e2e      |
| `libs/aje/chains/ton/`      | `subwallet.ts` derives genuine `v4r2` contract addresses (`workchain:sha256(StateInit)`, StateInit data = `seqno(u32) ‖ subwallet_id(u32) ‖ public_key(256) ‖ plugins`); per-invoice `subwallet_id` from a monotonic counter; canonical v4r2 code BoC loaded via `@ton/core` `Cell.fromBoc`. **Trust-tier C.** | sandbox e2e      |
| `libs/aje/chains/ergo/`     | `p2pk-allocator.ts` derives real P2PK addresses from a watch-only xpub (`@scure/bip32` `HDKey`); `ERG_BIP44_PURPOSE=44`, `ERG_SLIP44_COIN_TYPE=429`, `ERG_BIP44_DERIVATION_PREFIX = m/44'/429'/0'/0`; UTXO-set polling; 5/10/30 tiers.                                                                         | testnet e2e      |
| `libs/aje/chains/tron/`     | `tron-address.ts` derives genuine base58check addresses; **USDT-TRC20 only** (native TRX not accepted). **Trust-tier C.**                                                                                                                                                                                      | nile testnet e2e |

### TRON address derivation — worked example

`tron-address.ts` is a good illustration of "chain-valid, not synthetic." TRON
reuses Ethereum's secp256k1 address scheme: take `keccak_256` of the
uncompressed public key's `X‖Y` (dropping the `0x04` tag), keep the **last 20
bytes**, prefix the network byte `0x41` (`TRON_ADDRESS_PREFIX`), and
base58check-encode with a **double-SHA-256** checksum.
`tronAddressFromPublicKey` normalizes the key onto the curve
(`secp256k1.Point.fromHex(...).toBytes(false)`, which throws if the point isn't
on-curve), and `isValidTronAddress` round-trips the decode so a caller can
cross-check an address before accepting payment.

The TRON USDT constants are pinned (`usdt-trc20.ts`):

| Constant                   | Value                                |
| -------------------------- | ------------------------------------ |
| `USDT_TRC20_CONTRACT`      | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` |
| `USDT_TRC20_NILE_CONTRACT` | `TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj` |
| `TRON_USDT_DECIMALS`       | `6`                                  |
| `TRON_CONFIRMATION_DEPTH`  | `19`                                 |

The indexer scopes TRC-20 `Transfer` events to those contract addresses only — a
transfer of the _wrong_ token contract to the deposit address is not counted.

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

`@oshun/payments-bridge` (v0.1.0, private) is the single Oshun-specific library
on top of Aje. Its job is to turn an observed on-chain confirmation into an
Oshun entitlement event, with V1's trust-tier disclosures, receipt signing,
refund queue, and customer/admin surfaces layered on. Its `src/index.ts`
re-exports nine modules:

```text
state-mapper.ts            trust-tier-disclosure.ts   oracle-aggregator/
cold-spend-queue/          receipt-signer/            entitlement-bus/
customer-surface/          admin-surface/             security-gates/
```

> **Layout correction.** Earlier docs listed `src/trust-tier-disclosure/` as a
> directory and `src/oracle-aggregator.ts`/`src/cold-spend-queue.ts`/
> `src/receipt-signer.ts` as flat files — it is the reverse.
> `trust-tier-disclosure.ts` is a flat file; `oracle-aggregator/`,
> `cold-spend-queue/`, and `receipt-signer/` are directories. There is **no**
> `webhook-router.ts`, **no** `crisis-suppression.ts`, and **no**
> `telegram-handoff.ts` in this package — those module names in older prose
> never existed here.

### The bridge is functionally standalone

This is the single most important correction to the architecture's earlier
framing. The narrative was "Aje stays library-only, consumed through this
bridge" — implying the bridge _adapts_ Aje's `Invoice` / `PaymentConfirmation` /
`Refund` state machine. **It does not.** `package.json` declares `@aje/chains`,
`@aje/oracles`, `@aje/payments`, `@aje/wallets`, `@oshun/audit-platform`,
`@oshun/event-bus`, and `@oshun/identity` as dependencies, but a
`grep -rn "from '@aje" src/` returns **nothing**, and `@oshun/event-bus` is
likewise never imported. The bridge **reimplements** the invoice/confirmation
model and the event-emitter seam rather than adapting Aje. This is honest to
record because every downstream claim about "the bridge maps Aje's types onto
the bus" is, today, aspirational: the consumption path exists in `package.json`
but not in code.

### `state-mapper.ts` — the confirmation engine

This is the real heart of the bridge. It defines its **own** vocabularies (not
Aje's):

- `AJE_INVOICE_STATUSES` =
  `draft | pending | paid | partial | refunded | expired | cancelled` (note: no
  `sent`/`viewed`/`overdue`; adds `pending`/`expired`).
- `AJE_CONFIRMATION_STATUSES` = `unconfirmed | confirmed | finalized` (not Aje's
  five-value ladder).
- `V1_PAYMENT_EVENT_TYPES` =
  `payment.invoice.settled | payment.invoice.underpaid | payment.invoice.expired | payment.refund.broadcast`.

`mapAjeStateToV1(input)` is a pure function from an `AjeInvoiceStateInput` to a
`V1PaymentEvent | null`. The mapping:

| Source status                               | Emitted event               | Notes                                                            |
| ------------------------------------------- | --------------------------- | ---------------------------------------------------------------- |
| `paid` (and confirmation meets requirement) | `payment.invoice.settled`   | `grantsEntitlement` is `true` when an `entitlementId` is present |
| `paid` (below depth)                        | `null`                      | not yet settled                                                  |
| `partial`                                   | `payment.invoice.underpaid` | `underpaidByAtomic = expected − observed` (clamped ≥ 0)          |
| `refunded`                                  | `payment.refund.broadcast`  |                                                                  |
| `expired` / `cancelled`                     | `payment.invoice.expired`   |                                                                  |
| `draft` / `pending`                         | `null`                      | nothing to emit                                                  |

`mapAjeStateToV1WithAudit` wraps that with an injected
`V1PaymentStateAuditAppender` and writes an immutable
`V1PaymentStateAuditRecord` (`{ immutable: true, … }`) via `toAuditRecord`
before returning. Amounts are `bigint` atomic units throughout, so there is no
float drift.

#### `CONFIRMATION_POLICY` — 32 assets, per-tier depth

`V1_PAYMENT_ASSETS` is exactly **32** entries: `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}`. Each maps to a
`ConfirmationPolicy { micro, standard, highValue }` — `requiredConfirmations`
selects the depth from the amount tier. Representative real values:

| Asset                                 | micro | standard | high-value | Why                                                |
| ------------------------------------- | ----- | -------- | ---------- | -------------------------------------------------- |
| `btc-onchain`                         | 0     | 1        | 3          | small payments accept 0-conf; large wait for depth |
| `ltc`                                 | 1     | 3        | 6          | faster blocks, deeper for high value               |
| `xmr`                                 | 10    | 10       | 20         | Monero's 10-block unlock baseline                  |
| `eth-mainnet`                         | 12    | 12       | 30         | reorg safety scales with value                     |
| `ada`                                 | 15    | 15       | 30         |                                                    |
| `erg`                                 | 5     | 10       | 30         |                                                    |
| `usdt-tron`                           | 19    | 19       | 19         | flat 19-block (`TRON_CONFIRMATION_DEPTH`)          |
| `matic-polygon`                       | 256   | 256      | 256        | one Heimdall checkpoint window                     |
| `sol` / `usdc-solana` / `usdt-solana` | −1    | −1       | −2         | **commitment sentinels**                           |

Solana has no block-depth notion that maps cleanly, so its "depth" is encoded as
a **commitment sentinel**: `SOLANA_COMMITMENT_CONFIRMED = -1` and
`SOLANA_COMMITMENT_FINALIZED = -2`. The private
`computeConfirmationMeetsRequirement` branches on these: `-1` accepts a
`confirmed`-or-`finalized` confirmation status; `-2` requires `finalized`;
`required === 0` accepts any non-negative observed count; and the default branch
demands `observedConfirmations >= required` **and** a `confirmed`/`finalized`
status. This is real domain logic — it would not pass against a random or
hardcoded return.

### `trust-tier-disclosure.ts` — the acknowledgement gate

V1 will not render a deposit address for a riskier rail until the customer has
acknowledged what they are paying with. This file is the source of truth for
that policy. It exports `RAIL_TIERS = ['A','B','C']`,
`ISSUER_TRUST_CLASSES = ['native','decentralized-issuer','central-issuer-with-freeze']`,
a per-asset `RAIL_REGISTRY` of `RailDescriptor`, and the helpers
`getRailDescriptor`, `railRequiresDisclosure`, `listSupportedAssets`, and
`gateInvoiceCreation`.

A `RailDescriptor` carries `tier`, `issuerTrustClass`, an English
`disclosureCopyEn`, and `requiresDisclosureAcknowledgement`. The copy is real,
specific, and reviewed by Lilith policy each release — e.g. Base reads "Base is
an OP-Stack L2; the sequencer is currently operated by Coinbase and may pause
transactions. Withdrawals to L1 require a 7-day challenge period," and
`usdt-tron` reads "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."

| Tier / class                          | Assets                                                                                  | Ack required? |
| ------------------------------------- | --------------------------------------------------------------------------------------- | ------------- |
| **A**, `native`                       | `btc-onchain`, `btc-lightning`, `ltc`, `xmr`, `eth-mainnet`, `ada`, `erg`               | No            |
| **B**, `native`                       | `sol`, and the EVM L2 rails `eth-base`, `eth-arbitrum`, `eth-optimism`, `matic-polygon` | Yes           |
| **B/C**, `central-issuer-with-freeze` | all USDC / USDT / DAI assets                                                            | Yes           |
| **C**, `native`                       | `ton`                                                                                   | Yes           |
| **C**, `central-issuer-with-freeze`   | `usdt-tron`, `usdt-ton`                                                                 | Yes           |

`gateInvoiceCreation(request)` returns `{ verdict: 'allow' }` when no
acknowledgement is required (Tier-A native rails) or when one is present;
otherwise it returns
`{ verdict: 'block', reason: 'disclosure-acknowledgement-required', disclosureCopy }`,
carrying the exact copy the surface must show. This is a fail-closed gate: a
USDC or TON invoice cannot mint an address until the customer's acknowledgement
timestamp is recorded.

### `receipt-signer/` — independently-verifiable receipts

A crypto receipt is signed with **Ed25519** (`@noble/curves/ed25519`) so that a
customer holding only the receipt and the chain can verify Oshun's claim without
trusting Oshun's API. `ReceiptSigner.sign()` canonicalizes the payload —
`JSON.stringify` with **deterministically sorted keys** and bigints encoded as
**decimal strings** to survive JSON — and signs those bytes; `verifyReceipt`
re-derives the same canonical bytes and checks the signature.

`ReceiptPayload` (`receipt-signer/types.ts`) carries `invoiceId`, `merchantId`,
`customerLocale`, `txId`, `blockHash`, `blockHeight`, `asset` (one of the **17**
`RECEIPT_ASSETS`), `amount` (bigint, chain smallest unit), `fiatCurrency`,
`fiatAmountMinor` (bigint cents), `confirmedAtUnixSeconds`, a `tax`
(`TaxBreakdown`, reused from the fiat billing receipt formatter via
`locale-tax.ts`), and a `verificationSnippet` — a real shell/curl invocation the
customer can run to reproduce the on-chain check.

> **Monero proof correction.** The Monero payment proof is a **2-tuple**, not a
> 3-tuple. `MoneroPaymentProof = { txKey, address }`. The `txId` is a separate
> top-level `ReceiptPayload` field, _not_ part of the proof tuple. Older prose
> describing `(txid, tx_key, address)` as the proof was wrong.

The `SignedReceipt` wraps the payload with `signatureHex` (hex Ed25519 signature
over the canonical JSON) and the `auditKeyId` that signed it. The BFF constructs
a `ReceiptSigner` only when a private key is configured (see below).

### `entitlement-bus/` — the emitter seam

`entitlement-bus/topics.ts` defines three topics —
`PAYMENT_INVOICE_CONFIRMED_TOPIC = 'payment.invoice.confirmed'`,
`PAYMENT_INVOICE_SETTLED_TOPIC = 'payment.invoice.settled'`,
`PAYMENT_REFUND_BROADCAST_TOPIC = 'payment.refund.broadcast'` — and a
`PaymentRail` union with **14** `crypto-*` values plus 4 fiat
(`fiat-stripe | fiat-adyen | fiat-paypal | fiat-generic`). Every event carries
`schemaVersion: 1` and a structurally-fiat-compatible shape (`fiatEquivalent`,
`chainAsset`, `chainAmount`, `…AtUnixSeconds`), so a consumer need not know
whether a payment came from Stripe or from a crypto rail — the `rail` field
distinguishes them. The emitter (`emitter.ts`) takes an **injected `publish`
function** and never imports `@oshun/event-bus`; this is the standalone seam
noted above.

> **Two corrections worth pinning.** (1) The file's doc-comment says "thirteen
> crypto rails," but the `PaymentRail` union actually lists **14**
> (`crypto-cardano` is included) — off by one. (2) There is an **internal
> event-vocabulary split** the code itself does not reconcile: `state-mapper.ts`
> emits `settled | underpaid | expired | refund.broadcast`, while
> `entitlement-bus/topics.ts` defines `confirmed | settled | refund.broadcast`.
> The two modules use different event sets; `payment.invoice.confirmed` is never
> produced by the state mapper, and `underpaid`/`expired` are not topics on the
> bus. Earlier docs conflated the two as one coherent vocabulary.

### `oracle-aggregator/` — fiat-rate median + rate-lock

`oracle-aggregator/` (`price-feed.ts`, `tor-egress.ts`, `rate-lock.ts`) composes
multiple price sources into a V1 fiat-rate median, routes the off-Aje sources
through a Tor egress, and holds a per-invoice **rate-lock** cache so the quoted
fiat amount is stable for the life of an invoice rather than drifting with spot
price between quote and settlement.

### `cold-spend-queue/` — refund signing path

`cold-spend-queue/` (`queue.ts`, `sweep-policy.ts`, `hw-signing-fixture.ts`,
`audit-attestation.ts`, plus `integration.test.ts`) models the refund/sweep
flow: queue an unsigned transaction artifact, apply the sweep policy, route it
through a hardware-signing fixture, and attach an **audit attestation per
signing event**. The unsigned-tx artifacts span chain families (PSBT, EIP-1559,
Monero unsigned transfer, Cardano CBOR, Solana versioned tx, TON external
message, Ergo `UnsignedTransaction`, Tron raw tx). The _queue logic and
attestation_ are real; the **air-gapped station, hardware co-signers, and
multisig vaults themselves** are operational, not provable in-repo.

### `customer-surface/` — paywall, QR, reminders, Telegram

`customer-surface/` is a substantial real surface, underdescribed by older docs:

- **From-scratch QR generation** — `qr-matrix.ts` and `qr-svg.ts` implement a QR
  matrix/SVG encoder (with `qr-format-bits.test.ts` verifying the format bits).
  This is a genuine V1 artifact, not a passthrough to `@aje/payments`.
- **`paywall-spec.ts`** + **`disclosure-gate.ts`** + **`asset-chain-filter.ts`**
  drive what the customer sees and which chains are offered.
- **`reminder-cadence.ts`** implements the renewal-invoice reminder cadence
  (7-day / 24-hour / 1-hour) described in the product docs — real code behind
  that claim.
- **`telegram-bot-router.ts`** is the _actual_ home of crisis suppression.
  `routeUpgradeCryptoCommand(input)` returns
  `{ action: 'suppress', reason: 'crisis-active' }` when
  `input.crisisState === 'active'`. This gates the Telegram `/upgrade` command —
  **not** "every invoice-creation path." (A separate, unrelated
  crisis-suppression policy lives in the BFF under
  `apps/oshun/bff/src/safety/`.)

### `admin-surface/` and `security-gates/`

Two real modules the bridge table never mentioned:

- **`admin-surface/`** (`explorer-urls.ts`, `invoice-timeline.ts`,
  `node-health-panel.ts`, `refund-initiation.ts`, `invariant-guards.ts`) is the
  operator-facing crypto billing surface — block-explorer links, an invoice
  timeline, a node-health panel, refund initiation, and invariant guards.
- **`security-gates/`** (`build-time-invariants.ts`, `chaos-tester.ts`,
  `disclosure-audit.ts`, `node-health-probes.ts`, `tabletop.ts`) implements
  build-time invariant scanning, chaos and tabletop testing, disclosure
  auditing, and node-health probing. These back the "tests cover" claims the
  prose only alluded to.

## How an entitlement is actually granted

The mechanism that turns a settled payment into a tier change lives in
`@oshun/billing-support` (`billing-aje-bridge.ts`), which the audit found had
previously been an _island_ with zero importers. It does two concrete things:

1. **Collapses six billing classes onto three product tiers.** `TIER_BY_CLASS`
   maps `EntitlementClass`
   (`free | starter | plus | pro | scholar | institutional`) onto
   `OshunEntitlementTier` (`free | pro | premium`): `free → free`,
   `starter/plus → pro`, `pro/scholar/institutional → premium`. So the product
   reads **one** tier vocabulary, not two.

   ```text
   free          -> free
   starter, plus -> pro
   pro, scholar, institutional -> premium
   ```

2. **Advances the subscription state machine from a settlement.**
   `applyPaymentSettlementToSubscription` takes an
   `AjePaymentSettlement { status: 'confirmed' | 'failed' | 'refunded', reference }`
   (normalized from the confirmation ladder) and drives the subscription state.
   Only the `ENTITLING_STATES` set — `trial`, `active`, `grace`, `restored` —
   actually entitles the customer; a subscription in `past-due` / `paused` /
   `canceled` / `lapsed` falls back to `free`, so a lapsed payment immediately
   de-entitles instead of stranding a stale tier. A `confirmed` settlement moves
   `trial`/`past-due`/`grace`/`paused`/`restored` → `active` and
   `canceled`/`lapsed` → `restored`.

This is the concrete entitlement linkage the architecture previously described
only abstractly.

## The BFF is the real consumer

Older prose implied apps consume Aje "through the bridge" without naming the
layer. The real consumer is the **BFF** (`apps/oshun/bff/src/payments/`):

- `routes/domain-stubs.ts` imports `listSupportedAssets` from
  `@oshun/payments-bridge` and serves the **real** asset catalog at
  `GET /v1/payments/methods` (not a fixture).
- `payments/payments-composition.ts` and `payments/quote-builder.ts` import
  `ReceiptSigner` and `V1PaymentAsset`. `resolvePaymentsReceiptSigner(env)`
  constructs a `ReceiptSigner` only when
  `OSHUN_PAYMENTS_RECEIPT_ED25519_PRIVATE_KEY` is set, else returns `null`.
- `payments/` also holds `invoice-store.ts`, `settlement-route.ts`, and
  `quote-builder.ts`, which back `/v1/payments/crypto/quote`,
  `/v1/payments/invoices`, and the entitlement route `/v1/entitlements/aaa`.

`@aje` itself appears **only in a BFF comment**: `server.ts` (~line 930) notes
that in-repo no settlement provisioner is injected, so the crypto-quote route is
**fail-closed (503, no address issued)**. It also notes that a real deploy
"constructs the provisioner (BTCPay/OpenNode/@aje) + binds a real runtime here."
That is an honest fail-loud seam, not a stub.

### Live settlement is provider-gated

Consequently the `sign-up-and-pay-crypto` walkthrough verdict is **"pass
(surfaces) / partial (settlement)"**: `/billing/crypto` and `/aaa-upgrade`
render, the `/v1/payments/*` and `/v1/entitlements/aaa` BFF endpoints respond,
the asset catalog is real, and receipt signing works. But **live wallet →
merchant settlement** needs a real external merchant integration
(BTCPay/OpenNode), which is a documented external dependency. The
`v1-completeness-audit-2026-06-22` lists this walkthrough as `partial` for
exactly this reason.

## Operational invariants (spec / policy)

These are stated in code comments and types and enforced where buildable, but
several are operational concerns the repo cannot exercise headless:

- **View-only on the app server.** Spend keys never reach the application layer;
  they live on an air-gapped signing station behind hardware co-signers.
- **Per-invoice fresh derivation.** Fresh derivation paths / subaddresses on
  every chain; address reuse is rejected at the invoice contract layer.
- **Sweep to a 2-of-3 multisig vault** per chain family (Bitcoin SegWit
  multisig, EVM Safe, Monero MMS, Solana SPL multisig, TON multisig, Cardano
  Plutus-script multisig, Ergo P2S multisig, Tron multisig).
- **Telemetry stores only the hash** of any per-invoice identifier (Monero
  subaddress, Solana reference pubkey, TON subwallet address).
- **No on-chain customer identifiers** — no `OP_RETURN`, no Monero `tx_extra`
  payment IDs, no Solana/TON memo customer IDs; enforced at the build layer by
  the chain modules' invariant scanning.
- **Mandatory trust-tier disclosures** on every Tier-B/Tier-C rail and every
  centrally-issued asset, with the acknowledgement timestamp recorded before
  address rendering (the `gateInvoiceCreation` gate above).

The multisig vaults, air-gapped station, hardware co-signers, Tor egress in
production, and any running node or live settlement could **not** be verified
in-repo and should be treated as spec until an operational deployment exists.

## Backlog and scope

The crypto settlement work is tracked under §23.1 (asset enum §23.1.1, state
mapping, per-tier confirmation policy, trust-tier registry, receipt signer,
entitlement bus). Payment- and dependency-level concerns are at deps§13.
Out-of-V1 Aje capabilities (DePIN, on-chain gaming, NFT, RWA, prediction
markets, intents, restaking, ZKP, governance, Sui/Move) ship as part of the
library but are not used by any V1 surface.

## Related

- [Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md)
  — product-facing companion
- [Lilith — Contemplative Policy Substrate](./substrate-lilith.md) — reviews
  disclosure copy and owns crisis-state policy
- [Isis — Generation Control Substrate](./substrate-isis.md) — sibling substrate
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md) — crisis
  suppression and safety posture
- [Security, Privacy, and Compliance](./security-privacy-compliance.md) —
  custody and key-handling posture
- [Communication Patterns](./communication-patterns.md) — the event-bus topics
  consumers subscribe to
- [Subsystem Glossary](./glossary.md) — substrate names and roles
- [../ARCHITECTURE.md](../ARCHITECTURE.md) — architecture hub
