Ledger
Ledger is a privacy-first personal finance tool that ingests messy bank CSV exports and produces a structured, categorised ledger. A small on-device classification pipeline handles the tedious sorting; a clean interface keeps the human in control of the edge cases.
- Role
- Sole designer & engineer
- Year
- 2025
- Status
- In progress
- Category
- Artificial Intelligence
- Tools & technologies
- Next.jsTypeScriptPythonscikit-learnDuckDBTailwind
- Links
- GitHub
The problem
Personal finance apps either demand full bank access or bury you in manual data entry. I wanted something that respected privacy, worked from plain exports, and did the boring categorisation without pretending to be a financial advisor.
Context
Built over a semester alongside a data-mining course. The constraint that shaped everything: no raw transaction data should ever leave the machine, which ruled out most hosted-model approaches and forced a lean, local pipeline.
Process
From spreadsheet to model
I started by hand-labelling a few hundred of my own transactions to understand the real distribution of categories — groceries and transport dominate, but the long tail of one-off merchants is where a naive classifier falls apart.
Rather than reach for a large model, I trained a compact TF-IDF + linear classifier on merchant strings. It runs in milliseconds, is fully explainable, and — crucially — can be retrained on-device as the user corrects it.
- Merchant-string normalisation to collapse noisy variants
- Confidence thresholds that route uncertain rows to human review
- Per-user correction loop that quietly improves accuracy over time
Designing for trust, not automation
The interface never hides an automated decision. Every categorised row shows its confidence, and low-confidence rows surface first. The goal was a tool that feels like a capable assistant, not a black box making claims about your money.
Research
- 01Interviewed six students about how they currently track spending — most had abandoned an app within a month.
- 02Benchmarked the linear classifier against a fine-tuned transformer; the accuracy gap (3.1%) did not justify the privacy and latency cost.
- 03Studied Stripe and Copilot Money for how they present financial data without overwhelming.
Key decisions
Local-first over cloud sync
All parsing and inference run in the browser or a local Python process. Sync is opt-in and encrypted. This closed off some convenience features but made the privacy promise real.
Explainable model over marginal accuracy
A linear model I could inspect beat a black box I could not — especially for a domain where a wrong guess erodes trust instantly.
Human-in-the-loop by default
Corrections are a first-class interaction, not an error state. The system is designed to be taught.
Implementation
The pipeline
Exports are parsed with a tolerant CSV reader that handles the wildly different formats banks produce. Rows land in an in-browser DuckDB instance for fast local querying, which made the analytics views feel instant even over years of data.
The classifier ships as a small serialised model; inference happens client-side. A thin Python service exists only for the heavier retraining step.
Gallery
Outcomes
- 92% categorisation accuracy on my own two years of transactions after one correction pass.
- Sub-100ms inference for a full year of data on a mid-range laptop.
- Currently dogfooding monthly; a small closed beta is planned.
Lessons learned
- The hardest part of an AI product is rarely the model — it is designing the moments where the model is wrong.
- Explainability is a feature users feel even when they never open the explanation.
- Local-first is a real design constraint, not just an engineering one; it changes what the product can promise.
Related work