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.
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.
13,407 outflows ≥ CHF 1 · largest gap: digit 7, 1.2 points · verified 2026-08-26
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.
2,184 payees with outflows · verified 2026-08-26
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.
n = 5,055 outflows under CHF 20 · verified 2026-08-26
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.
Transactions per payee, 2015–2026 · verified 2026-08-26
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.
GROUP BY payee, amount HAVING months ≥ 6 · verified 2026-08-26
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.
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 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
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.
| table | rows | what it holds |
|---|---|---|
transactions | 16,094 | date · account · category · payee · amount_chf · fx_rate · source |
payees | 2,284 | the long tail nobody plans for |
categories | 101 | the taxonomy that makes reports possible |
accounts | 97 | banks · cards · brokers · entities |
valuations · properties | 81 | what a ledger cannot see by itself |
fx_rates · budgets | 1,891 | three 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