← crisgrossmann.com

The Ledger · a personal finance data lake

My accountant sees my books once a year. This sees them every morning at 03:30.

Eleven unbroken years of transactions from 97 accounts in one Postgres schema. I did not set out to study my own spending. I set out to stop typing it twice — and the patterns showed up on their own.

16,094
transactions
97
accounts
CHF 6,200
recovered by catching errors
99.2%
categorised

My spending obeys a law used to catch accounting fraud

Benford's Law says that in naturally occurring numbers, the leading digit is a 1 about 30% of the time, and each digit after that is rarer. Auditors use the deviation from it to flag fabricated books. I never aimed at this. Eleven years of groceries, taxes and Ubers land on the curve anyway.

132.3 217.7 313.1 48.8 58.6 66.4 74.6 84.4 94.2 mine Benford

13,407 outflows ≥ CHF 1 · largest gap: digit 7, 1.2 points · verified 2026-08-26

Twenty names out of 2,284 hold 84% of the money

Pareto is a cliché until you run it on your own life. Twenty payees — under one percent of the list — absorb five sixths of everything that ever left. And the other end is stranger: 58% of every business I have ever paid, I paid exactly once.

84.4%
of all outflow volume sits with 20 payees — 0.9% of the list
1,267
payees paid exactly once in eleven years — 58% of them all

2,184 payees with outflows · verified 2026-08-26

Four of every ten lines in my books are worth one thousandth of the money

5,055 transactions are under CHF 20. That is 37% of everything my system has to import, deduplicate, convert, categorise and store. Together they account for 0.10% of the francs. This is the strongest argument for automation I have ever run: the work does not scale with the value, so the work has to cost nothing.

Share of lines37.4%
Share of francs0.10%

n = 5,055 outflows under CHF 20 · verified 2026-08-26

One supermarket has been in my life longer than any job, city or bike

Coop shows up in twelve different calendar years, from 31 December 2015 to last month: 1,158 separate visits. It is the single most persistent relationship in the dataset. Beekeeper, the company I founded, is second at 216 lines. Uber and Migros follow at eleven years each. A ledger is a biography written by someone who was not paying attention.

Coop1,158
Uber436
Migros427
Amazon298
Beekeeper AG216

Transactions per payee, 2015–2026 · verified 2026-08-26

The system found 88 subscriptions I never wrote down

Same payee, identical amount, month after month: that pattern is a standing order whether or not you remember signing it. There are 88 of them across 38 payees, and six have been running for more than two years. The longest has charged the same figure for 38 consecutive months. Nobody has a list like this in their head. That is the point of writing it down in a machine.

88
recurring patterns found · 38 payees
38
consecutive months for the longest one, same amount every time

GROUP BY payee, amount HAVING months ≥ 6 · verified 2026-08-26

A transaction crosses five systems before it rests

Nothing here is typed twice. The bank exports, the importer normalises three currencies into francs, the categories come from sixteen years of accounting in Xero and, for anything new, my own rules, the lake stores it, and a nightly job proves the whole thing still exists tomorrow. A failure pushes to my phone before I wake up.

3 sourcesbanks · cards importerfx → CHF Xero + rules101 buckets finance.* Postgres schema16,094 rows dashboard+ accountant backup 03:30+ 03:45 dump

Twice, the ledger paid for itself in a single line

Everything above is structure. This part is money that came back. Neither of these was found by staring at a bank statement — both surfaced because every transaction has to land in a schema where a duplicate looks wrong and a missing pattern leaves a hole.

CHF 4,000
A furniture order charged twice. The same payee, the same amount, days apart. In a spreadsheet it reads as two purchases. In a ledger with 2,284 payees and eleven years of habits, it reads as an anomaly. Flagged, disputed, refunded.
CHF 2,200
An incoming payment that stopped arriving. Recurring inflows are the easiest thing in the world to not notice: nothing breaks, nothing beeps, the money simply is not there. The pattern broke, the gap showed, and it was recovered while it was still recoverable.

CHF 6,200 recovered from two mistakes that nobody made on purpose and nobody would have caught by feel. That is the honest return on eleven years of typing things into the right columns.

Both caught by the reconciliation pass, not by memory

The schema, so you can steal it

There is no product here to sell you. Eleven years fit in six tables and one foreign key discipline: every transaction points at an account, a category and a payee, and nothing else is allowed to be a string typed by hand.

tablerowswhat it holds
transactions16,094date · account · category · payee · amount_chf · fx_rate · source
payees2,284the long tail nobody plans for
categories101the taxonomy that makes reports possible
accounts97banks · cards · brokers · entities
valuations · properties81what a ledger cannot see by itself
fx_rates · budgets1,891three currencies, one truth
-- the query behind finding #3: the work does not scale with the value
select count(*) filter (where abs(amount_chf) < 20) lines_under_20,
       round(100.0 * sum(abs(amount_chf)) filter (where abs(amount_chf) < 20)
             / sum(abs(amount_chf)), 2) pct_of_francs
from   finance.transactions
where  amount_chf < 0;
-- → 5,055 lines · 0.10% of the money
Numbers verified against the lake on 26 August 2026. No balances, no net worth, no account numbers. The only francs on this page are the ones the system got back.
Sister pages: the health lake · the swarm · race engineering