← Back

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.

GitHub