A local-first personal finance pipeline powered by Claude Code. Drop a bank statement PDF into statements/ and get structured transaction data, a full interactive dashboard, and spending insights — no cloud service, no API keys, no data leaving your machine.
statements/
└── your-statement.pdf
│
▼ /process-statements
│ Claude reads the PDF, extracts every transaction
│
▼ /clean-data
│ Dedup, normalise, assign subcategories
│ → output/processed/cleaned_cumulative.csv
│
▼ python scripts/gen-dashboard-data.py
│ Converts CSV → dashboard/data/transactions.js
│
▼ open dashboard/Ledger.html
10-view interactive web dashboard
Claude Code acts as the AI brain: it reads raw PDFs, understands each bank's layout, extracts all transactions, normalises them to a consistent schema, and saves the output. No OCR library, no custom parser per bank — just Claude reading and reasoning.
- Multi-bank support — N26 (checking + Spaces), American Express PAYBACK, Advanzia Gebührenfrei Mastercard
- Rich schema — 18 fields per transaction including subcategory, foreign currency, exchange rate, city, payment method, N26 Space sub-account, internal transfer flag
- Idempotent pipeline — files tracked by SHA256 hash; re-running never double-counts
- Ledger web dashboard — 10-view static HTML/React app, no server needed
- Streamlit dashboard — legacy 7-page Python dashboard (still works)
- Claude Code slash commands —
/process-statements,/pipeline-status,/reprocess,/clean-data,/start-dashboard - Privacy first — all data stays local;
.gitignoreensures statements, extracted data, and the dashboard data file are never committed
Prerequisites: Python 3.11+, Claude Code
git clone https://github.com/pranaymodukuru/personal-finance-ops
cd personal-finance-ops
pip install -e .No API keys required — Claude Code is the runtime.
Drop any supported bank statement PDF into the appropriate subfolder:
statements/
├── N26/
│ └── n26-october-2024.pdf
├── Amex/
│ └── amex-november-2024.pdf
└── Advanzia/
└── advanzia-december-2024.pdf
Inside Claude Code, run:
/process-statements
Claude reads each unprocessed PDF, extracts all transactions, and saves structured output to output/raw/.
/clean-data
Deduplicates, normalises receivers, flags internal transfers, and assigns subcategories. Writes output/processed/cleaned_cumulative.csv.
python scripts/gen-dashboard-data.py --serveThis generates dashboard/data/transactions.js (gitignored — personal data stays local) and opens http://localhost:8765/Ledger.html in your browser.
Note: do not open
Ledger.htmldirectly viafile://. Chrome blocks loading of local.jsxscripts that way, resulting in a blank page. The--serveflag starts a lightweight HTTP server that avoids this.
| Command | Description |
|---|---|
/process-statements |
Find all unprocessed PDFs in statements/ and extract transactions |
/pipeline-status |
Show processed files, transaction counts, and what's pending |
/reprocess [file] |
Force re-extract a specific statement even if already processed |
/clean-data |
Clean and enrich output/raw/cumulative.csv → output/processed/cleaned_cumulative.csv |
/start-dashboard |
Instructions for opening Ledger or starting Streamlit |
/reset |
Delete all statements, extracted data, and pipeline state (asks for confirmation first) |
A warm editorial-light web app (Monzo / Copilot aesthetic) built with React and inline SVG charts. Opens as a static HTML file — no build step, no server.
| View | What you see |
|---|---|
| Dashboard | KPI cards (spend, income, savings rate), monthly bar chart, recent transactions, top categories |
| Net worth & forecast | Cumulative balance across all accounts + 6-month projection (dashed) |
| Transactions | Full searchable/filterable table; inline category and subcategory editing |
| Spending breakdown | Donut chart; click any category to drill into subcategories and merchants |
| Merchants | Leaderboard ranked by spend with horizontal bar chart |
| Subscriptions | Auto-detected recurring charges by cadence and amount stability |
| Budgets & goals | Editable monthly envelope budgets + savings goals with progress bars |
| Income & savings | Savings rate trend (interactive line chart), month-by-month breakdown |
| Investments | ETF flow tracking |
| Travel & FX | Foreign currency transactions, spend by currency |
Features: privacy mode (blur amounts), dark/warm/cool themes, compact/comfy density, accent colour picker, internal transfer toggle, time range selector (this month → all time), mobile companion view.
Updating after new statements: re-run /process-statements → /clean-data → python scripts/gen-dashboard-data.py --serve, then reload the tab.
Every transaction is normalised to 18 fields:
| Field | Type | Description |
|---|---|---|
date |
YYYY-MM-DD | Booking/settlement date |
transaction_date |
YYYY-MM-DD | Date the purchase occurred (differs on Amex) |
receiver |
string | Counterparty name — merchant, person, or institution |
reference |
string | Payment reference or note |
description |
string | Short clean English description |
amount |
decimal | Negative = debit, positive = credit. Always EUR. |
currency_original |
string | null | ISO 4217 code if original currency was not EUR |
amount_original |
decimal | null | Amount in original currency |
exchange_rate |
decimal | null | FX rate used |
type |
debit | credit |
Direction of the transaction |
category |
string | food, transport, utilities, entertainment, healthcare, shopping, income, savings, transfer, fees, travel, learning, insurance, other |
subcategory |
string | null | Finer-grained label (e.g. groceries, etf_investments, streaming) |
payment_method |
string | null | paypal, sepa_transfer, direct_card, direct_debit, atm |
account_type |
string | checking, savings (N26 Spaces), credit_card |
space |
string | null | N26 Space name (e.g. "House Rent") |
city |
string | null | City/location if stated in document |
bank |
string | "N26", "Amex", "Advanzia" |
is_internal_transfer |
boolean | True if money moved between own accounts |
personal-finance-ops/
├── statements/ # Drop PDFs here — git-ignored
│ ├── N26/
│ ├── Amex/
│ └── Advanzia/
├── output/
│ ├── raw/
│ │ ├── individual/ # Per-statement CSV + JSON — git-ignored
│ │ └── cumulative.csv # All transactions combined (raw) — git-ignored
│ └── processed/
│ └── cleaned_cumulative.csv # Cleaned + enriched — git-ignored
├── dashboard/ # Ledger web dashboard (static HTML/React)
│ ├── Ledger.html # Entry point — open this in your browser
│ ├── styles.css
│ ├── helpers.jsx
│ ├── charts.jsx
│ ├── pages-1.jsx # Overview, Transactions, Spending, Budgets
│ ├── pages-2.jsx # Income, Investments, Subscriptions, Merchants, Travel, NetWorth
│ ├── mobile.jsx
│ ├── tweaks-panel.jsx
│ ├── ios-frame.jsx
│ └── data/
│ └── transactions.js # Generated by scripts/gen-dashboard-data.py — git-ignored
├── scripts/
│ └── gen-dashboard-data.py # CSV → transactions.js converter
├── src/
│ ├── extractor.py # PDF → raw text via pdfplumber (fallback)
│ ├── pipeline.py # Tracks processed files by SHA256 hash
│ ├── save.py # Writes CSV/JSON and updates pipeline.json
│ ├── cleaner.py # Cleaning pipeline: dedup, normalise, flag, subcategorise
│ └── subcategory.py # assign_subcategory() — regex-based classification
├── .claude/
│ └── skills/
│ ├── process-statements/SKILL.md
│ ├── pipeline-status/SKILL.md
│ ├── reprocess/SKILL.md
│ ├── clean-data/SKILL.md
│ └── start-dashboard/SKILL.md
├── dashboard.py # Streamlit dashboard (legacy, 7 pages)
├── pipeline.json # Auto-generated; tracks processed files — git-ignored
├── CLAUDE.md # Instructions for Claude Code
└── pyproject.toml
| Bank | Type | Notes |
|---|---|---|
| N26 | Checking account | Supports Spaces (sub-accounts), value date vs booking date, sender IBAN |
| American Express PAYBACK | Credit card | Two dates per transaction, PayPal merchant detection |
| Advanzia Gebührenfrei Mastercard | Credit card | City field, inline FX with exchange rate |
- Drop a statement PDF into
statements/ - Run
/process-statements— Claude identifies the format and extracts accordingly - If the layout is complex, run
python -m src.extractor statements/yourfile.pdfto inspect raw text first
All processing is local. .gitignore ensures the following are never committed:
statements/— your bank statement PDFsoutput/raw/individual/— per-statement extracted filesoutput/raw/cumulative.csvandoutput/processed/*.csv— transaction historydashboard/data/transactions.js— dashboard data filepipeline.json— file hashes and processing metadata
The repo contains only code, configuration, and documentation.