FinLedger
Personal finance ledger · weekly reconciliation · local-first
Problem
AI finance apps suck at calculating expenses. They import your transactions and act like the hard part is done. Unstable IDs, duplicates, and conflicting signs are only the start. Card spend, brokerage cash flow, and payroll deductions live in different domains with different schemas. A friend paying you back could be dinner, rent, or a gift, and a wink emoji does not settle it.
Solution
I built FinLedger as a local-first Python/SQLite app that unifies four heterogeneous feeds into one append-only ledger, with explicit domain isolation so investment activity never hits expense reconciliation.
Each Sunday I sync feeds, run a rules-first reimbursement matcher that abstains when it is not sure, and resolve the rest on a review desk. Confirmed matches only affect reported net spend. Human decisions compile into export bundles so AI agents do not re-decide settled items.
On a 92-example hand-labeled gold set, production rules-only matching hits 100% auto-match precision (33/33 at ≥0.75 confidence) and 47.8% auto recall. The matcher abstains instead of guessing, so false auto-matches stay at zero. Rules plus an optional LLM note gate pushes recall to 52.2% with the same 100% precision and no new errors.
Python · SQLite · vanilla JS · MCP
Local-only personal tool. Demo uses synthetic UI data.
How it works
FinLedger runs on your machine with a SQLite store at
~/.finledger/ledger.db. One adapter per feed normalizes
card, Venmo, brokerage, and payroll data into a shared transaction
schema. A single api.py layer serves the CLI, a stdlib
HTTP server on localhost, and a hand-rolled MCP JSON-RPC server with
18 namespaced query tools.
Three read surfaces sit over one store: expenses (spend, runway, reimbursements), investments (balances, holdings, cash flow), and income (payroll waterfall from gross through taxes and deductions to net). Investment activity never crosses into expense reconciliation. The desk UI is vanilla HTML/CSS/JS with no framework, tabbed for the weekly sync and reconciliation workflow.
| Layer | Technologies |
|---|---|
| Language | Python 3.10+ |
| Database | SQLite (append-only upsert-by-id) |
| API / server | Python stdlib (http.server, JSON REST) |
| CLI | finledger entry point (argparse) |
| Frontend | Vanilla HTML/CSS/JS tabbed dashboard |
| Agent layer | Hand-rolled MCP JSON-RPC over stdio (18 tools, no SDK) |
| Matcher | Sibling repo expense-reconciliation (rules-first, abstain path) |
| Feed | Integration |
|---|---|
| Card + Venmo | shim.finance REST API, Plaid-style sign convention |
| Card backfill | CSV parsing + content hashing |
| Brokerages + checking | SnapTrade REST with HMAC auth |
| Payroll | Paystub PDF regex parser (PayStatement schema) |
Reconciliation runs rules-first with a review queue for abstentions.
Confirmed matches only affect net spend. Human resolves persist across
re-syncs. Export snapshots and Claude bundle round-trips carry
policy.json and decisions.json so agents
reason over live numbers with settled policy baked in, not open
questions.
Matcher eval
Scored on the hand-labeled gold set in
expense-reconciliation (92 peer reimbursement examples).
Auto-match metrics are what actually ships without human review at
≥0.75 confidence. Exact-set accuracy is stricter: the predicted charge
set must match gold exactly, including empty sets for
not-a-reimbursement.
| Metric | Rules-only (rules_v0) |
Rules + LLM |
|---|---|---|
| Auto-match precision | 100% (33/33) | 100% (36/36) |
| Auto-match recall | 47.8% (33/69) | 52.2% (36/69) |
| Exact-set accuracy | 60.9% (56/92) | 64.1% (59/92) |
Most misses are correct review candidates that did not cross the auto threshold, not wrong auto-matches. The baseline matcher on the same set was 8.9% auto precision.
Local-only personal tool. No auth or multi-tenant. Core package is stdlib-only; the only external dependency is the sibling reconciliation package.