SPLITTER
Shared expenses and personal finance, exact to the cent
- Situation
- Roommates and travel groups split shared costs, and everyone needs to see the same numbers.
- Task
- I had to prove the app settles a group in the fewest payments, and measure how often a scanned total is wrong.
- Action
- I added a seeded property test, moved splits to integer math, and built a receipt eval gated in CI.
- Result
- Every split adds up to the cent, and when the scanner fills a total it is right 105 times in 106 (synthetic receipts).

500/500seeded test groups settle in the fewest possible payments (simple greedy method: 368/500)
A group pays each other back in fewer transfers, and a scanned receipt total is left blank rather than guessed.



How I built it
Added a seeded property test. Every balance settles, amounts are whole positive cents, nobody relays money, and the payment count equals the fewest possible, from an independent exact search.
Memoized debt simplification. The exact search runs inside a fixed step budget, with a greedy fallback that still settles everyone.
Moved weight splits to exact integer math, so a tie goes to the earlier person, as documented.
Built an offline receipt eval: synthetic receipts in three noise tiers, run through the app's own OCR settings and parser.
Gated receipt precision and coverage in CI, and added an authorization test generated from the database catalog.
Why these choices
Integer cents over floating point
Splits, weights and by-day shares use exact integer math with largest remainder, so parts always sum to the total and ties go to the earlier person.
Invariants in the database
Membership, totals, item shares and currency conversions are recomputed in Postgres, because the anon key is public and the browser is not trusted.
Tests on the real schema, not mocks
Tests load the real schema into in-memory Postgres (PGlite) and call the real functions. A schema-only bug that migration-based tests hid for three weeks is now covered.
A step budget over a wall-clock cap
The search stops after a fixed number of route combinations, so every device shows the same payments for the same ledger. The cost: a 9 or 10 person group can block the page for seconds the first time it renders.
How it was measured
- Debt simplification on 500 seeded circle groups: exact search 500/500 at the minimum, plain greedy 368/500 (Oct 23, 2026). On 500 random groups, a sanity check where greedy also reaches the minimum, every group settled with no invariant failures, and payments fell from 7,741 to 2,432, 68.6% fewer.
- Receipt scanner: 200 synthetic receipts in three noise tiers, run through the app's own OCR settings and parser. Totals right 105 of 106 filled (99.1%, 95% CI 94.8 to 99.8%), with 94 of 200 left blank (Oct 22, 2026).
Failures and limits
- Weight splits used floating point. The parts still summed to the total, but ties broke by rounding instead of going to the earlier person.
- Caution costs coverage. OCR left 94 of 200 totals blank, often because junk on the total line broke the parser's strict pattern.
- Merchant names are weak under OCR: right in 100 of 173 suggestions, mostly because junk rides along with the name.
- The exact search is slow on the largest groups. On 10-person groups it took up to 10.8 seconds on a heavily loaded laptop and fell back to greedy 17 times in 50, still settling everyone. It runs on the main thread, so moving it to a worker is the next fix.
- The receipts are synthetic. Real photos with curled paper and faded ink are not measured.