Personal Money Dashboard
A weekend. The 0-to-60 finish line. By the end you'll have a background daemon that pulls fresh transactions from your bank every few hours into a local SQLite database, and an MCP server that exposes that database to Claude Desktop — so you can open the GUI and ask "how much did I spend on coffee in March?" and get a real answer from your real data.
It also introduces a security pattern worth learning for life: how to collaborate with Claude on handling a secret without ever showing Claude the secret.
What you'll need
- macOS or Linux.
- Python 3.10+.
- A SimpleFIN Bridge account at bridge.simplefin.org. It's the only sane option for personal use — Plaid isn't realistically available to individuals. Cost: about $1.50/month. After you link your bank, SimpleFIN gives you a one-time setup token (a long base64 string).
- Claude Desktop installed (claude.ai/download). The terminal
claudeCLI is great for building this; the MCP integration we wire up at the end lights up inside the Desktop app.
Your SimpleFIN setup token is the equivalent of bank-account read access. We will let Claude write the code that handles it — but we will not let Claude read it.
- You write the setup token to a file on disk, by hand:
~/simplefin-setup-token.txt. - You tell Claude: "There is a setup token at that path. Do not read it. Write a script that reads it, calls SimpleFIN's claim endpoint to convert it to an access URL, writes the URL to
~/.config/finance-sync/.envwith chmod 600, and then shreds the original file." - You run the script. The token gets consumed by SimpleFIN's one-shot claim endpoint, the access URL lands in
.env, and the original file is shredded (shred -uon Linux,rm -Pon macOS). - From here on, Claude can read
.envif it needs to test sync — but the production code reads via env vars, not bycat-ing the file.
Claude is perfectly capable of writing a secret-handling pipeline without seeing the secret. The bytes never enter the conversation. This is a habit worth keeping — for API keys, OAuth tokens, app passwords, anything you don't want Claude to memorize or accidentally print.
Setup walkthrough
- Sign up at SimpleFIN Bridge, link your bank(s), and copy the setup token they give you.
- Write the token to a file by hand. Do not paste it into chat.
mkdir -p ~/.config/finance-sync nano ~/simplefin-setup-token.txt # or vim, or TextEdit chmod 600 ~/simplefin-setup-token.txt - Make the project folder:
mkdir -p ~/projects/money && cd ~/projects/money git init - Start Claude Code and paste the prompt below. The first thing Claude does is generate
bootstrap_secret.py— the safety-hack script. Read it before you run it. - Run it:
Verify:python3 bootstrap_secret.pycat ~/.config/finance-sync/.envshould have a singleSIMPLEFIN_ACCESS_URL=...line. The original token file should be gone. - Let Claude finish the rest — the sync code, the SQLite schema, the categorizer with
rules.yaml, and the MCP server. Commit after each phase. - First sync:
finance sync --dry-run finance sync finance summary - Schedule the background sync every 6 hours —
launchdplist (macOS) orsystemd --usertimer (Linux). Claude will write the file and the one-line command to enable it. Logs go to~/.local/state/finance-sync/. - Wire the MCP server into Claude Desktop. Claude will print the exact JSON snippet for
claude_desktop_config.json. Restart Claude Desktop, open a new conversation, and ask:How much did I spend on groceries in the last three months?
Claude Desktop will call the MCP server you just built. The server reads your local SQLite. None of your transactions leave your computer except as part of the question you initiated.
The prompt
Help me build a personal money dashboard. I'm doing this in four phases —
build them in order, commit after each, and ask me to verify before moving on.
Phase 0 — Secret bootstrap (SAFETY-CRITICAL, READ CAREFULLY).
There is a SimpleFIN setup token at ~/simplefin-setup-token.txt. DO NOT
READ THAT FILE. Do not cat it, do not include its contents in any tool
call, do not print it.
Write bootstrap_secret.py that:
- Reads the token from that file path.
- POSTs to the URL contained in the token (SimpleFIN claim flow:
base64-decode the token to get a URL, POST to that URL with no body,
the response body is the access URL).
- Writes SIMPLEFIN_ACCESS_URL=<url> to ~/.config/finance-sync/.env with
permissions 0600 (create the parent dir if needed).
- Shreds the original token file: `shred -u` on Linux, `rm -P` on macOS;
fall back to overwriting with random bytes + unlink if neither exists.
- Prints "Bootstrapped." and exits 0 on success. Prints a clear error and
exits nonzero on any failure, WITHOUT leaking the token to stdout/stderr.
Stop after Phase 0 and let me run the script and verify. Don't proceed
until I confirm.
Phase 1 — Sync.
Build a `finance` CLI that pulls transactions from SimpleFIN and stores
them in SQLite at ~/.local/share/finance/finance.db.
SimpleFIN access URL is read from ~/.config/finance-sync/.env. Use HTTP
basic auth (the URL itself is of the form https://USER:PASS@host/path —
split it).
Schema (keep it minimal, no ORM):
accounts(id TEXT PRIMARY KEY, name, currency, institution)
transactions(
id TEXT PRIMARY KEY, -- SimpleFIN transaction id
account_id TEXT,
posted INTEGER, -- unix seconds
pending INTEGER, -- 0 or 1
amount REAL, -- signed; negative is debit
description TEXT, -- raw
category TEXT, -- assigned by rules.yaml later
hash TEXT -- sha256 for safety dedup
)
sync_runs(started_at INTEGER, finished_at INTEGER, ok INTEGER, error TEXT)
CLI:
finance sync Pull last 90 days, insert new transactions
(idempotent on transactions.id; also de-dupe by hash).
finance sync --dry-run Print counts of would-insert / would-skip.
finance accounts List accounts and balances.
finance recent [-n 50] Print most recent transactions.
Hard requirements:
- Idempotent: running sync twice in a row never duplicates rows.
- Pending -> posted transitions update the existing row, don't insert.
- Never print the access URL or its credentials to logs.
- All HTTP timeouts default to 30s.
- Use only: httpx, click, python-dotenv. Stdlib for sqlite3.
Stop after Phase 1 and let me run `finance sync` against real data.
Phase 2 — Categorization.
Add `rules.yaml` in the project folder, gitignored (it may contain merchant
names). Format:
rules:
- match: "STARBUCKS|BLUE BOTTLE"
category: "Coffee"
- match: "WHOLE FOODS|TRADER JOE'S"
category: "Groceries"
- match: "^UBER\\b"
category: "Transport"
default: "Uncategorized"
Each rule's `match` is a case-insensitive regex against the description.
First match wins. Add:
finance categorize Apply rules; update `category` column. Idempotent.
finance categorize --dry-run
Also write a starter rules.yaml.example seeded with ~15 common US merchant
patterns.
Phase 3 — MCP server for Claude Desktop.
Build an MCP server (`finance-mcp`) using the official MCP Python SDK,
stdio transport. Read-only access to the same SQLite DB.
Tools to expose:
summarize_spending(start_date, end_date, group_by)
group_by in ("category", "month", "merchant"). Returns a list of
{key, total, count}.
search_transactions(query, limit=50)
Description regex match, optional date range.
list_categories()
Distinct categories with counts.
recent_transactions(n=20)
Most recent N transactions.
The MCP server must NEVER expose write tools. Queries are read-only and
parameterized.
In the README, include the exact JSON snippet to drop into
claude_desktop_config.json for both macOS and Linux paths, plus the
"restart Claude Desktop" step.
Project layout:
money/
pyproject.toml
.env.example (SIMPLEFIN_ACCESS_URL=)
.gitignore (.venv, .env, rules.yaml, *.db)
bootstrap_secret.py
rules.yaml.example (committed; the real rules.yaml is gitignored)
src/finance/
__init__.py
cli.py
sync.py
db.py
categorize.py
mcp_server.py
scripts/
com.user.finance-sync.plist (macOS, generated only when I ask)
finance-sync.service (Linux)
finance-sync.timer
README.md
README sections, in order:
1. What this is (1 paragraph)
2. The safety hack (one paragraph)
3. Phase-by-phase setup
4. Scheduling background sync (launchd / systemd, with the exact `launchctl
load` / `systemctl --user enable --now` commands)
5. Connecting Claude Desktop (the JSON snippet, the restart step,
example questions to try)
6. Troubleshooting
Code quality bar: small, readable, no unnecessary abstractions. No Pydantic.
No "models" layer. Dict-based rows are fine. Tests only for categorize()
and the dedup logic.
When you're ready, start with Phase 0 and STOP for my confirmation.
When something goes wrong
bootstrap_secret.pysays the claim failed — usually one of two things. (1) The token already got claimed (they're one-shot). Re-issue at bridge.simplefin.org. (2) Your network is intercepting TLS. Try from a normal connection.- Sync inserts zero rows but you know there are transactions — check
finance sync --dry-run. SimpleFIN only returns recent activity by default; ask Claude to add a--since YYYY-MM-DDflag. - Claude Desktop doesn't see the MCP server — fully quit and reopen. Check the JSON syntax — a stray comma will silently disable everything. The Desktop app has a Developer / MCP log if you turn it on; that's the fastest way to see why a server isn't loading.
- Claude Desktop sees the server but gives nonsense answers — your
categorycolumn is mostlyUncategorized. Editrules.yaml, runfinance categorize, ask again.
Tips
rules.yamlis the file you'll edit most. Tune it once a week for a month and your categorization will be excellent. Don't let Claude "improve" it without asking — it'll over-engineer.- The MCP integration is the moment everything pays off. It's the difference between a script you forget and a tool that's there, in your normal Claude conversations, whenever you have a money question.
- Back up the DB. It's small. Throw
cp finance.db finance-$(date +%F).dbin a weekly cron. Or ask Claude. - Apply the safety hack to other secrets. Anywhere you have an API key, OAuth refresh token, or app password — use the same pattern. Write to disk by hand, let Claude write the consumer/shredder, never paste in chat.
claude, and tell it what you want. You already know the loop.