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
-
The engine
Trifold's own fold
KPI hash19f08b80…7d48
same hash
-
hledger
plain-text accounting
KPI hash19f08b80…7d48
same hash
-
DuckDB
SQL over the events
KPI hash19f08b80…7d48
same hash
The run, verbatim
atm is the engine's name until the rename to tfd lands.
$ 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”.
$ 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.
$ 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:
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.
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.
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.
04 Time zones
The whole check runs again under America/Denver and Asia/Tokyo. The output is byte-identical to the goldens under both.
05 Read-only means read-only
The database and the folder are hashed before and after every read-only command, to show nothing changed.
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.
| Org | Preset | Members | Invoices | Refunds | Disputes | Gross collected | Paying last month | KPI hash |
|---|---|---|---|---|---|---|---|---|
| Ironbark Studio | studio-membersseed 201 · 2026-01..2026-12 | 73 | 512 | 3 | 1 | $47,527 | 45 | c315b659…ca91 |
| Ironbark Studio | studio-class-packsseed 202 · 2026-01..2026-09 | 40 | 199 | 1 | 2 | $17,771.63 | 22 | cdbb0c6d…587e |
| Cedar Hill Community Church | church-givingseed 301 · 2026-01..2026-12 | 82 | 608 | 5 | 0 | $68,050 | 52 | b87fa3cb…7904 |
| Lumen Notes | saas-seatsseed 401 · 2026-01..2026-12 | 89 | 515 | 3 | 1 | $69,521 | 49 | cd0acf28…69f6 |
| Lumen Notes | saas-annualseed 402 · 2026-01..2027-12 | 20 | 38 | 0 | 0 | $52,993 | 0 (all yearly) | 84d6c33c…3f36 |
| Night Kitchen Courses | creator-launchseed 501 · 2026-01..2026-12 | 134 | 851 | 61 | 6 | $25,297 | 66 | 38e8325c…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.
$ 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
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.
Sources
Read it for yourself.
- The golden end-to-end check, explained ↗(opens in a new tab) packages/atm/docs/golden-e2e-check.md
- The test that runs it ↗(opens in a new tab) packages/atm/tests/golden-e2e-check.test.ts
- The scenario knobs ↗(opens in a new tab) packages/atm/src/scenario/params.ts
- The simulated orgs ↗(opens in a new tab) packages/atm/docs/scenario-orgs.md
- The engine's source ↗(opens in a new tab) packages/atm
- The live demo ↗(opens in a new tab) Honeycomb Hill Academy, synthetic data
Read the source. Run the check.
Books ✓Receipt ✓Stripe ✓
Design partners: founder onboarding, free during the pilot.