Skip to content

Proof · reproducible · on GitHub

Proof, not promises.

Run the simulator on your own machine and get the same hashes we did. Here is how we check the engine, what the checks found, and where they stop.

Same numbers, three ways

One school year. Three tools. One hash.

The simulator generates a seeded, synthetic school year, puts it through the engine's fold, hledger and DuckDB, and compares one KPI hash per channel. Same seed, same events, same hash.

One sample school year

growth-12mo · seed 7 · 1,964 events

  1. The engine

    Trifold's own fold

    KPI hash19f08b80…7d48

    same hash

  2. hledger

    plain-text accounting

    KPI hash19f08b80…7d48

    same hash

  3. DuckDB

    SQL over the events

    KPI hash19f08b80…7d48

    same hash

One KPI hash per channel. All three match, so the three tools agree on every KPI.

The run, verbatim

atm is the engine's name until the rename to tfd lands.

packages/atm · simulate
$ bun ./src/bin.ts simulate --profile growth-12mo --seed 7
scenario growth-12mo (seed 7): 1964 events, 2026-01..2026-12
fixture hash c5e322c7d39c63fd44c4a61d4bcb88eb92bf5ca26f7f3e6a9cff3a7cc59186af
kpis hash by channel:
  atm      19f08b808b15d1fce659314f23067a48424b1dc16d51e2a3e79c599a673b7d48
  hledger  19f08b808b15d1fce659314f23067a48424b1dc16d51e2a3e79c599a673b7d48
  duckdb   19f08b808b15d1fce659314f23067a48424b1dc16d51e2a3e79c599a673b7d48
parity: every channel that ran has the same hash

Try it

It needs bun. bun install sets up the whole monorepo workspace, not just the engine. hledger and DuckDB are optional: a channel that isn't installed is skipped, which is why the last line says “every channel that ran”.

try it
$ git clone https://github.com/justbry/atm && cd atm && bun install
$ cd packages/atm && bun ./src/bin.ts simulate --profile growth-12mo --seed 7

The live demo is checked the same way: every dashboard number is checked against the engine's own export. Open the live demo ↗(opens in a new tab)

What would you simulate?

Turn the knobs. The hashes still match.

Pick a preset, change what you like, and pick a seed. Here: 120 families, 5% leaving each month, 8% of payments refunded, 30% paying for the year.

packages/atm · simulate
$ bun ./src/bin.ts simulate --profile monthly-members --set members=120 --set churnRate=0.05 --set refundRate=0.08 --set yearlyShare=0.3 --seed 42
scenario monthly-members (seed 42): 5794 events, 2026-01..2026-12
fixture hash 88662ef16ad63f969d8984bcb2743071513ef4d5fbee0ca063612351388b1e5e
kpis hash by channel:
  atm      2de5997d9be71c3d3c099217459fe251b24bdfe1f5f852a6e8b778f58c1b4b27
  hledger  2de5997d9be71c3d3c099217459fe251b24bdfe1f5f852a6e8b778f58c1b4b27
  duckdb   2de5997d9be71c3d3c099217459fe251b24bdfe1f5f852a6e8b778f58c1b4b27
parity: every channel that ran has the same hash

The knobs

months
months simulated, 1 to 36
start
the first month, YYYY-MM
members
families that join in month one, 0 to 200
newMembersPerMonth
families that join each later month, 0 to 50
tierMix
how new families pick among the tiers
yearlyShare
the share of families that pay yearly
addonRate
the chance a family buys an add-on at sign-up
freeShare
the share whose invoices are fully discounted: credits, no money
churnRate
the chance a family cancels at a renewal
refundRate
the chance a paid invoice is refunded
partialRefundShare
the share of refunds that are partial
disputeRate
the chance a paid invoice is disputed
disputeLostShare
the share of closed disputes that are lost
usageRate
how often a family books a class with its credits
currency
usd, eur, gbp or cad
feePercent
Stripe's percentage fee on a payment
feeFixedCents
Stripe's fixed fee on a payment, in cents
seatChangeRate
the monthly chance a seat count moves up or down (default 0)
upgradeRate
the monthly chance a monthly plan moves up a tier (default 0)

From params.ts(opens in a new tab), where each knob's range and meaning are defined.

The presets

bun ./src/bin.ts scenarios list lists every preset with its org: the school's (free-only, monthly-members, annual-prepay, heavy-refunds, disputes and more), then a studio, a church, a SaaS product and a course creator, below in Simulations.

The golden end-to-end check

The real CLI, compared byte for byte.

One test drives the engine the way you would, then compares everything it writes with goldens committed to the repo. From the full run recorded on 2026-11-05:

  1. 01 The real CLI

    Each command runs as its own process, the way you'd run it, over 46 scenario bundles and two generated school years. The bundles' Stripe events are hand-written from Stripe's documented shapes or recorded in a Stripe sandbox.

  2. 02 Byte for byte

    Every output is compared byte for byte with goldens committed to the repo, and hledger's own checks run on the journals.

  3. 03 Crash drill

    The commands that ship records to the folder are killed with SIGKILL mid-operation, then run again. 72 trials; in 62 the kill landed mid-operation. All 72 reruns were identical to an uninterrupted run.

  4. 04 Time zones

    The whole check runs again under America/Denver and Asia/Tokyo. The output is byte-identical to the goldens under both.

  5. 05 Read-only means read-only

    The database and the folder are hashed before and after every read-only command, to show nothing changed.

  6. 06 Erasure drill

    After a family is erased, its personal strings are searched for in every file and every table.

Read how the check works ↗(opens in a new tab) Read the test ↗(opens in a new tab)

What the check found

Four findings, in plain words.

A check that never finds anything isn't checking. These are open, and we say so.

  • F1

    A what-if from the folder can be less complete

    When the history holds a refused invoice, a what-if run from the shipped folder is less complete than one from the database. It shows up in 2 of the 46 bundles. No money is involved.

  • F2

    Erased facts linger until a checkpoint

    After an erasure, the erased facts (ids, amounts, times) stay in SQLite's write-ahead log until the next checkpoint. Never names or emails, which are never stored.

  • F3

    The first ship of a large backlog is slow

    Shipping a very large backlog for the first time is slow. We've identified an index fix.

  • F4

    What-if memory grows with history

    A what-if run's memory grows with the size of the history: about 2 GB for a year of 1,000 families.

Each finding is pinned in the test. It fails when a new one appears or a known one goes away, so this list can't quietly go stale.

Where it stops

What this doesn't prove.

  • These are fixtures and simulations, not a real organization's data. The Stripe events are hand-written from Stripe's documented shapes or recorded in a sandbox; a live account can still do something we haven't seen.
  • Matching hashes show three tools agree on the same events. They don't show the events match what happened at the bank.
  • A simulation is a model of an organization, not a forecast of yours.

Simulations · invented names · synthetic data

A studio, a church, a SaaS product, a course creator.

Four invented orgs beside the school, each on the same billing rules, the same books and the same three-way check. None of it is a real organization's data.

OrgPresetMembersInvoicesRefundsDisputesGross collectedPaying last monthKPI hash
Ironbark Studiostudio-membersseed 201 · 2026-01..2026-127351231$47,52745c315b659…ca91
Ironbark Studiostudio-class-packsseed 202 · 2026-01..2026-094019912$17,771.6322cdbb0c6d…587e
Cedar Hill Community Churchchurch-givingseed 301 · 2026-01..2026-128260850$68,05052b87fa3cb…7904
Lumen Notessaas-seatsseed 401 · 2026-01..2026-128951531$69,52149cd0acf28…69f6
Lumen Notessaas-annualseed 402 · 2026-01..2027-12203800$52,9930 (all yearly)84d6c33c…3f36
Night Kitchen Coursescreator-launchseed 501 · 2026-01..2026-12134851616$25,2976638e8325c…62ad

Every row: atm, hledger and DuckDB gave the same KPI hash.

Members is new_paying_members; invoices is payments_count; paying last month is paying_members_last_month. On saas-annual every plan is yearly, so nobody pays in the last month. Hashes show the first eight and last four characters.

Run them

From packages/atm. scenarios describe prints the org, its knobs and copy-paste commands. Any org takes an ad-hoc scenario on stdin.

packages/atm · scenarios
$ bun ./src/bin.ts scenarios list
$ bun ./src/bin.ts scenarios describe church-giving
$ bun ./src/bin.ts simulate --profile saas-seats --json
$ echo '{"org":"creator","params":{"members":40,"refundRate":0.1}}' | bun ./src/bin.ts simulate --scenario - --json

How the orgs are simulated ↗(opens in a new tab)

Where the simulations simplify

Each org is the closest honest fit to the billing engine, not an exact copy of a real business.

  • Everyone who pays is a member.
  • A one-time purchase (a gift, a course, a drop-in) comes only with a member's first invoice, and each member has one add-on at most.
  • Add-on money is booked to the member's tier, not a separate income line.
  • Church giving is four fixed recurring amounts: no pledges, no custom amounts, no campaign goals.
  • SaaS usage is counted in blocks of 1,000 calls.
  • A launch spike is the founding month.
  • There's no MRR KPI yet.

Receipts and seals

Signed receipts and sealed months ship today.

Every Stripe payment gets a receipt signed with Ed25519, and each month can be sealed. Check the sample receipt in your browser, with no account.

Verify a sample receipt

Sources

Read it for yourself.

Read the source. Run the check.

Books ✓Receipt ✓Stripe ✓

Design partners: founder onboarding, free during the pilot.