A local-first personal spending analyzer. Point it at the CSV exports your bank already gives you and it figures out what each transaction was, finds the subscriptions you forgot about, tracks your budgets, and shows the whole picture on a dashboard that runs entirely on your machine.
No accounts, no bank credentials, no cloud. Your financial data never leaves your computer.
pip install -e .
penny demo # loads 18 months of realistic sample data and opens the dashboardI kept exporting my bank statements into a spreadsheet to answer the same three questions (how much did I spend on eating out last month, what's this recurring charge, am I over budget) and rewriting the same pivot tables every time. Every tool that does this properly wants you to hand over your bank login. I wanted something that reads a plain CSV, keeps everything on my laptop, and is small enough that I actually understand what it's doing with my money.
The interesting engineering problem turned out to be telling a subscription
apart from a habit: Netflix on the 4th of every month is a subscription;
groceries every Saturday for a different amount is not. That detector is the core
of the project; see docs/RECURRING.md.
| Reads your bank's CSV | Built-in parsers for Chase, Amex, Capital One, Revolut/Wise, plus generic single-amount and debit/credit layouts. Anything unrecognised gets a point-and-click column mapper. Handles $1,234.56 and 1.234,56, (45.00) and 45.00 DR, BOM/latin-1, ;-delimited, preamble junk lines. |
| Categorises automatically | 174 built-in merchant rules + fuzzy matching against merchants you've already filed + a "learn from my correction" loop. Fix a category once and Penny writes a personal rule that wins forever. |
| Finds recurring charges | Detects weekly → yearly cadences from the gaps between charges, flags price increases ("↑ from $13.99 in Nov"), predicts the next charge, and tells you what you spend per month and per year on subscriptions. |
| Budgets | Per-category monthly limits with an end-of-month projection and over/watch/ok status. Suggests limits from your 3-month average. |
| Dashboard | Month overview, 12-month trend, category donut, top merchants, upcoming charges, all rendered with hand-written SVG (no charting library). Light / dark / system themes. |
| De-duplicates | Re-importing the same statement is a no-op; overlapping date ranges across files are merged; pending-vs-posted near-duplicates are flagged. |
| Money is never a float | Every amount is integer minor units end to end. |
| CLI too | penny import, penny summary, penny subscriptions for people who live in the terminal. |
| Recurring charges | Transactions | Import |
|---|---|---|
![]() |
![]() |
![]() |
git clone https://github.com/Samprit67/penny
cd penny
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
penny demo # sample data + dashboard at http://127.0.0.1:8765With your own data:
penny import ~/Downloads/chase-statement.csv --account "Chase Checking"
penny import ~/Downloads/amex-activity.csv --account "Amex Gold" --kind credit
penny serve # dashboard
penny subscriptions # or just print the recurring chargesEverything is stored in a single SQLite file
(~/Library/Application Support/penny/penny.db on macOS,
$XDG_DATA_HOME/penny on Linux). Delete that file and Penny forgets everything.
CSV bytes
│ ingest/detect.py sniff encoding, delimiter, header row, best parser
▼
BankParser.parse() → RawTxn(date, signed cents, description)
│ categorize/ normalise merchant → rule engine → fuzzy fallback
▼
Transaction rows (SQLite, via SQLAlchemy)
│ analyze/recurring.py gap analysis → cadence → impostor rejection → describe
│ analyze/summary.py month totals, category breakdown, vs last month
│ analyze/budgets.py actual vs limit, end-of-month projection
▼
FastAPI ──────────────► vanilla-JS SPA + hand-rolled SVG charts
Each analysis module is a pure function over a list of transactions; the database is only touched by a thin wrapper at the bottom of the file. That's what makes the test suite fast and the logic easy to reason about.
More detail: docs/ARCHITECTURE.md ·
docs/RECURRING.md ·
docs/PARSERS.md ·
docs/PRIVACY.md
- Python 3.10+ · FastAPI + Uvicorn · SQLAlchemy 2.0 (typed models) over SQLite (WAL) · Typer CLI
- Frontend: vanilla ES modules, no build step, SVG charts written by hand (
penny/web/charts.js) - Core engine dependencies:
python-dateutil,pyyaml, and that's it - Tooling: ruff (lint + format), mypy (strict on the core), pytest + Hypothesis, GitHub Actions
pytest # 129 tests, ~2s
pytest --cov=penny # ~85% line coverage
ruff check penny tests && ruff format --check penny tests
mypy pennyThe recurring-charge detector has its own test file
(tests/test_recurring.py) covering clean monthly,
jittered monthly, one- and two-step price changes, annual-from-two-charges,
weekly/quarterly, missed cycles, and the cases that must be rejected:
irregular spending, and groceries that happen to be weekly. Money parsing and
the fuzzy matcher are checked with property-based tests.
- One currency per account (no FX conversion between accounts).
- The whole dataset is loaded into memory for analysis, which is fine for years of personal data, not for a business ledger with millions of rows.
- Subscription detection needs ≥ 3 charges to see a monthly pattern (2 for annual). A brand-new subscription won't show up until its third month.
- CSV in, no OFX/QIF yet; no direct bank connections by design.
- OFX / QIF import
- Split transactions across categories
- "Cancel this" checklist for unused subscriptions
- CSV / Excel export of any filtered view
- Recurring-income detection (paychecks, dividends)
- Optional encryption at rest
The small stuff (the $4 coffees, the $12 subscription you forgot about) is where the money actually goes. Penny keeps count.
MIT. See LICENSE.



