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.

[Run it yourself](https://tfd-ledger.pages.dev/proof#three-ways) [What the check found](https://tfd-ledger.pages.dev/proof#findings)

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 ↗](https://staging.b4uni.com/for/schools/dashboard)

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](https://github.com/justbry/atm/blob/dev/packages/atm/src/scenario/params.ts), 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](https://tfd-ledger.pages.dev/proof#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 ↗](https://github.com/justbry/atm/blob/dev/packages/atm/docs/golden-e2e-check.md) [Read the test ↗](https://github.com/justbry/atm/blob/dev/packages/atm/tests/golden-e2e-check.test.ts)

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.

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 ↗](https://github.com/justbry/atm/blob/dev/packages/atm/docs/scenario-orgs.md)

### 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](https://tfd-ledger.pages.dev/verify)

Sources

## Read it for yourself.

-   [The golden end-to-end check, explained ↗](https://github.com/justbry/atm/blob/dev/packages/atm/docs/golden-e2e-check.md) packages/atm/docs/golden-e2e-check.md
-   [The test that runs it ↗](https://github.com/justbry/atm/blob/dev/packages/atm/tests/golden-e2e-check.test.ts) packages/atm/tests/golden-e2e-check.test.ts
-   [The scenario knobs ↗](https://github.com/justbry/atm/blob/dev/packages/atm/src/scenario/params.ts) packages/atm/src/scenario/params.ts
-   [The simulated orgs ↗](https://github.com/justbry/atm/blob/dev/packages/atm/docs/scenario-orgs.md) packages/atm/docs/scenario-orgs.md
-   [The engine's source ↗](https://github.com/justbry/atm/tree/dev/packages/atm) packages/atm
-   [The live demo ↗](https://staging.b4uni.com/for/schools/dashboard) Honeycomb Hill Academy, synthetic data

## Read the source. Run the check.

Books **✓**·Receipt **✓**·Stripe **✓**

[Get early access](https://tfd-ledger.pages.dev/early-access) [Verify a sample receipt](https://tfd-ledger.pages.dev/verify)

Design partners: founder onboarding, free during the pilot.
