A House Divided A House DividedDocumentation
Changelog
Engineering/Architecture

Shadow ledger: double-entry conservation checking

Last updated 2026-08-20
Source files

The shadow ledger is a double-entry journal layered over the game's single-entry financialTxLog. It exists to structurally catch the silent money-corruption bug class, FX-mismatch writes (the t841 nationalization blowup, 2,300× inflation), ghost counterparties (the 15.3T ITL carry trade), unfunded interest, dropped/double debits, and "doesn't debit / reverts next turn" tickets, by asserting a conservation law over every ₳ movement and alarming the same turn a violation happens instead of turns later.

It is shadow-only (Phases 1-3): the game never reads it and behaviour never changes. It observes and alarms. Enforcement (rejecting unbalanced writes) is a later, separately gated phase. Full design: game repo the design archive and the src/lib/ledger/ module (accounts.ts, deriveFromTx.ts, emit.ts, reconcile.ts, report.ts, types.ts).

The ledgerShadow flag#

A gameConfig feature flag (flags-as-seed-defaults, PR #2627 style): on by default in prod seeds (). When on, emitTx/emitTxBulk () also derive a balanced ledger entry from each tx-log row, and the reconciler runs each turn. When off, none of the collections below are written, a trace_ledger call on such a world correctly reports "no data".

The two collections#

ledgerEntries, the journal#

One balanced entry per money event, derived per transaction. Each has ≥2 legs; the sum of the legs' anchorAmount (the ₳-snapshot value at emit time) must be 0 within ε. Same-currency legs must also net to 0 in native units, that second check is what catches the t841 class (raw NGN stored as GBP balances in neither). Legs carry a role (primary = the authoritative balance that moved; contra = the counter-leg, either a real counterparty or a system mint:/sink: bucket). Single-sided rows get a mint:unattributed / sink:unattributed leg, deliberately ugly, it is the Phase 3 coverage work queue. emitSite records the grep-able origin.

ledgerReconciliations, the per-turn verdict#

One doc per reconciler run, with an overall status (green / amber / red) and three sub-checks, each with their own status and findings:

unattributed on the doc ranks the single-sided mint/sink legs by |anchor|, the Phase 3 backlog. A third collection, balanceSnapshots, holds per-turn authoritative balances and is the stock side of the stock-vs-flow check.

How to read a reconciliation#

Forensic tool: trace_ledger#

Lives on the gamestate MCP (ahd-ops-dashboard/mcp/gamestate-server.js, port 9730), alongside trace_character / trace_corp / trace_election / trace_sector. Read-only.

Given a turn (default: latest reconciliation on record; otherwise the newest at or before the requested turn) it returns the reconciler verdict, the unbalanced entries and divergent accounts with their emit sites, the money-supply residuals, and the ranked unattributed legs. Add an account (canonical id, e.g. character:<id>:USD, government:US:USD, fx:NGN/GBP) or an entity fragment (a raw character/corp/party/country id, matches any account containing it) to also pull every journal entry (both legs) touching it that turn, plus that account's balance-snapshot delta. That is the direct path from "there's ghost money" to the exact unbalanced entry, emit site, and source doc that produced it.