The money module: what it owns, and the invariants that are easy to break. The rules for any
balance change are in docs/standards/money.md; the crypto-rail rules are in
docs/standards/custody.md. packages/core/src/wallet/AGENTS.md routes between them.
docs/catalog.json is the exhaustive list of this module's tables, routes and events. This file
does not repeat it.
- The ledger. A player's balance is a row here, and its transaction history is immutable. No chain and no vendor holds a per-player balance.
- The asset catalog. The currency-and-network pairs the operator accepts, and for each pair the vendor's own asset identifier, the minimums, the fee, and independent switches for deposit and withdrawal. Which pairs exist is operator configuration, not code.
- The custody surface. Issued deposit addresses, saved payout destinations, per-player vendor containers, the sweep, reconciliation findings, and the withdrawal approval path.
- A currency does not identify a chain. Anything that reconciles, prices, or limits works on the currency and the network together. Only a fiat rail and an internal movement have no network.
- Every ledger row records its direction explicitly. Direction is never inferred from the kind of transaction: a player-to-player transfer writes the same kind for the sender's debit and the recipient's credit.
- A deposit, withdrawal or manual adjustment records its reference value once, when written.
The reference currency is the player's deposit-limit currency, else their wager-limit currency,
else
wallet.defaultReferenceCurrency. The rate and its timestamp are stored with it and never recalculated. With no fresh rate the row is not written: a request fails with a typed error, and a deposit webhook fails so the vendor redelivers it. - A withdrawal's network fee comes out of the amount the player entered. The player is debited the full amount, the row records the fee, and the provider pays out the amount minus the fee. An amount that does not exceed the fee is refused. A refund returns the full debited amount.
- Whether a withdrawal needs KYC is compliance's decision, asked through
KYC_WITHDRAWAL_POLICY.kyc.gateWithdrawalsswitches the request-time check on; auto-approval always asks. Without a bound policy, every gated withdrawal needs an approved KYC status. - Cross-module money moves through the wallet's command port, inside the caller's transaction. Another module never reads or writes wallet tables directly, and a transfer is never settled over an event.
- A vendor call is not transactional. Persist a recoverable state first, make every settlement transition idempotent, and compensate a failed held withdrawal exactly once.
- The balance stream carries a signal, not an amount. A dropped frame must not be able to leave a stale number on screen, so the client refetches. Every event that moves a settled balance publishes one, both legs of a player-to-player transfer included.
- Admin actions are guarded and audited. The guard is the first line of the handler. A manual adjustment, an approval, a rejection, a catalog edit and a resolved finding each write an audit entry naming the actor and the reason.
- The webhook path resolves the verifier and the adapter from the same provider entry, and fails closed when either is missing.