Contents

The proof

The independent prover: one report that re-derives conservation, backing, no-overdraft, and chain integrity from the ledger itself.

An “invariant” is a rule that must hold true at every moment, no matter what just happened — for instance, that no account can ever go negative. economy-lab fixes a short list of these as its definition of a correct economy, and this page is about the check that catches any one of them breaking.

Source src/integrity.ts#L88proveEconomysrc/contract.ts#L664ProveReportscripts/prove.ts

API proveEconomy

The idea

You don’t have to take “the books balance” on faith: the prover rebuilds every money invariant from scratch, straight from the ledger’s posted legs, and hands you one ProveReport telling you whether the books still hold.

It only reads, it never writes. It sums the debit and credit lines itself instead of trusting any cached balance. It recomputes each account’s hash chain from genesis. Then it compares the result against what the ledger actually stored.

You call it three ways. read.health() runs it on demand. make prove runs it after every operation in a randomized program. make fuzz runs it against every backend and checks they agree.

const report = await economy.read.health();
report.conserved &&
  report.backed &&
  report.noOverdraft &&
  report.chainIntact &&
  report.consistent;
// → true: books balance, every credit backed, nothing overdrawn,
//   no entry altered, no cached balance drifted

Why it exists

The write path can only vouch for the writes it performs. The invariants are enforced at write time in the database (see Integrity); the prover is the second line of defense, an independent audit on top.

Its job is to catch the failures a write-path guard structurally can’t see. A bug in the enforcement. A half-applied write. A row edited directly in the database. That is proveEconomy’s stated contract: an independent audit, not the enforcer — it never guards the write path (see source).

Anyone holding the ledger can re-derive the answer. The proof doesn’t depend on trusting the process that wrote the rows.

The invariant it maintains

The prover checks five properties and reports each as a boolean flag on ProveReport. It maintains none of them on its own; it observes them. A false flag means an upstream invariant was already violated before the prover looked.

FlagHolds when
conservedDebits and credits cancel to zero in every currency, so no value was minted or lost.
backedTRUST_CASH holds at least custodial credits × par in real USD, every spendable credit is covered (see Solvency).
noOverdraftNo wallet account has gone below zero.
chainIntactEvery account’s hash chain recomputes to its recorded head, no posted entry was altered.
consistentEvery account’s cached balance equals the sum of its posted legs; drift is empty.

Two more fields carry the detail behind a failing flag. shortfall is the missing USD when backed is false, and zero otherwise. drift lists each account whose cached balance disagrees with its legs. Each drift entry names both the materialized and the derived figure, so an operator can see the size and direction of the gap.

allInvariantsHold(report) is the convenience roll-up. It is true only when all five flags are true (see source). If you only care about one property, read its field directly.

How it’s enforced

The whole proof runs in one pass over the ledger. foldLedger walks every account, sums each account’s legs, and folds those signed amounts into a per-currency total that must reach zero. conserved is driven by the derived sum rather than the stored balance, so a mis-saved balance surfaces as drift instead of hiding a real imbalance. backed and noOverdraft read the cached balance directly, which is cheaper and is exactly the figure they vouch for.

The fold doesn’t repeat the chain check: proveEconomy hands it to proveChain so the hash chain is verified in one place.

Backing is the one flag that crosses currencies. It converts custodial credits to USD at the par rate ((custodialCreditMinor × par.rate) / 10^par.scale, rounded down) then compares against the live TRUST_CASH balance. Any positive gap is the shortfall.

What relies on it

The treasury worker job re-checks backing every cycle, and two verification scripts double as CI gates.

make prove drives the real economy through a seeded-random program (top-ups, promo grants, spends) and calls read.health() after every committed operation, failing on the first violation. Each run is 8 seeds × 60 operations per backend. After each step it does two more checks:

  • It resubmits the operation to confirm a retry returns duplicate (replayIsDuplicate).
  • It checks that each touched account’s stored head matches the hash the operation reported (verifyChainLinks).

On the first violation, make prove shrinks to the shortest failing prefix and exits non-zero (scripts/prove.ts).

make fuzz runs the same workload against every backend: memory, the in-process http adapter, postgres, and mysql. For the same inputs, it checks they produce byte-identical balances, chain heads, and ProveReports. A backend that drifts from the reference is caught right away. Unreachable backends are skipped, not failed.

Everything on this page holds per operation. The two remaining concept pages cover the write path under hostile load: a runaway caller in Spend velocity, and contending writers in Concurrency.

Try it

The prover runs on this page. The snippet moves real credits through the same engine build the console drives, then asks read.health() for the five-flag report over the books your run just changed:

import { credits, spend, systemActor, topUp, userActor } from '@pwngh/economy-lab';

import type { Economy } from '@pwngh/economy-lab';
import type { SnippetReport } from './context.ts';

// Move real money, then ask the ledger to prove itself: the five invariant flags,
// re-checked over the state your operations just changed — the same report the
// console's Integrity page polls.
export async function run(economy: Economy): Promise<SnippetReport> {
  const orderId = `ord_${crypto.randomUUID().slice(0, 8)}`;
  await economy.submit(
    topUp({
      idempotencyKey: `idem_${orderId}`,
      actor: systemActor('docs'),
      userId: 'usr_alice',
      amount: credits(250),
      source: 'card',
    }),
  );
  await economy.submit(
    spend({
      idempotencyKey: orderId,
      actor: userActor('usr_alice'),
      orderId,
      buyerId: 'usr_alice',
      sku: 'Proof Demo Pass',
      price: credits(250),
      recipients: [{ sellerId: 'usr_nova', shareBps: 10_000 }],
    }),
  );

  const p = await economy.read.health();
  const allGreen = p.conserved && p.backed && p.noOverdraft && p.chainIntact && p.consistent;
  const mark = (ok: boolean) => (ok ? 'holds' : 'FAILED');

  return {
    lines: [
      `conserved: ${mark(p.conserved)} · backed: ${mark(p.backed)} · ` +
        `no overdraft: ${mark(p.noOverdraft)}`,
      `chain intact: ${mark(p.chainIntact)} · consistent: ${mark(p.consistent)}`,
      allGreen
        ? 'all five re-derived and holding after your operations'
        : 'a check failed — that would be a bug worth reporting',
    ],
    consolePath: '/integrity',
  };
}

The console’s Integrity page re-runs the same five checks on every load — after replaying the operations you just ran here.

See also