I used to spend around two hours a week touching my finances. Personal spend in one Excel file, business income and expenses in another, crypto scattered across Binance statements, Nexo interest CSVs, and a third spreadsheet where I logged what I'd actually bought. At year end, a £500 tax advisor would turn it all into a self-assessment.
I spent a weekend building a replacement. It's been running for months now. This is what's in it.
What it is
finance-dashboard is a FastAPI app with a SQLite database behind it and a vanilla JavaScript SPA on top. No React, no build step, no cloud. It runs on an Ubuntu 24.04 LXC container on my home Proxmox box, reachable only over VPN on port 8080.
Rough size:
-
main.py: 4,226 lines, 76 API endpoints -
app.js: 4,818 lines - SQLite schema: 22 tables
- Total: about 9,800 lines across 6 files
Heavy lifting comes from three libraries: pdfplumber for PDF parsing, openpyxl for Binance Excel exports, and httpx for the async calls out to CoinGecko (live crypto prices), frankfurter.app (historical USD/GBP rates), and Perplexity (AI-generated crypto analysis).
There is no Binance or Coinbase API integration. Imports happen via uploaded files. That was deliberate: I wanted the source of truth to be the statements themselves, not a live feed I'd have to trust.
The personal split: 55/15/15/15
Every month I enter my PAYE net pay. The app splits it four ways:
- 55% Outgoings
- 15% Invest 1
- 15% Invest 2
- 15% Fun
Those percentages and bucket names live in the settings table, so they're configurable if the rule ever changes. The split is computed, not moved. There's no ledger of transfers between buckets, just a running remaining figure per bucket:
budget_remaining_outgoings = budget_outgoings
- fixed_total
- outgoings_spent
- food_spent
Fixed outgoings (mortgage, phone, gym, Apple Music) are snapshotted per month. If my gym price goes up, last year's budget doesn't silently shift. That's a small thing that matters the first time you look back at a 9-month-old month and wonder why the numbers don't reconcile.
A separate month_invest_status table tracks whether each 15% chunk actually got invested that month. That's the checkbox that forces the discipline.
The business split: 60/40
One setting: business_tax_pct = 60. Every time business income or an expense changes, _recalc_tax_pot fires and walks all months in chronological order to recompute a cumulative running total:
tax_pct = float(settings.get("business_tax_pct", 60)) / 100
total_business = sum of month's business_income
amount_added = round(total_business * tax_pct, 2)
# upsert, then recompute running_total across all months
60% of every pound of business income is flagged as tax pot. The remaining 40% is "running costs allocation". It's pessimistic on purpose. I'd rather over-reserve and release at year end than under-reserve and scramble.
The UK tax calculation
This is the piece a generic accounting tool won't give you. _calculate_uk_tax() is about 240 lines and encodes the full 2025-26 self-assessment picture:
- Personal allowance £12,570, tapered by £1 for every £2 above £100k
- Basic 20%, higher 40%, additional 45%
- Class 4 NI: 6% between £12,570 and £50,270, 2% above
- Working-from-home flat-rate tiers (101+ hours a month = £26, 51-100 = £18, 25-50 = £10)
- Trading allowance vs actual expenses, whichever gives the better deduction
- Crypto income filtered to the exact 6 April to 5 April window
It aggregates P60 data where available, falls back to summed monthly payslips, adds P11D benefits, and subtracts PAYE already paid to estimate what's owed on self-assessment plus the two payments on account.
It also tracks six separate allowances in one view: savings interest (£500), dividends (£500), CGT (£3,000), ISA (£20,000), trading (£1,000), and WFH. That's the screen I miss most when I imagine going back.
CGT on crypto is its own beast. The code implements the HMRC matching rules properly: same-day matching, then the bed-and-breakfast rule (buys within 30 days after a sell), then the Section 104 pool for everything else.
The crypto profit-taking panel
Two things drive it, and neither is a "sell now" alarm.
First, a target allocation for the portfolio (BTC 30%, HBAR 30%, SOL 20%, ADA 10%, NEAR 10%). Live prices from CoinGecko get compared to those targets, and anything overweight generates a rebalance hint like "Sell 0.042 SOL". That's a nudge, not a signal.
Second, an "Analyse with AI" button per holding that sends the asset's cost basis, unrealised P&L, and allocation to Perplexity's sonar-deep-research model with a prompt asking for a HOLD / ACCUMULATE / REDUCE call, price targets, and the UK CGT implications of selling now. Results cache in a crypto_analysis_cache table so I'm not re-running expensive queries.
It's research on tap, grounded in my actual numbers.
What broke: the PDF import parser
This is where I spent most of the weekend, and where I've been back to patch things since.
The expense PDF parser is ~170 lines of regex. Every new vendor layout I import exposes a new edge case. The scars:
- USD detection requires an actual
$sign, because GBP amounts near the word "Total" would otherwise match. Upwork uses "Total payments $111.99", Stripe uses "Amount due US$49.00", generic invoices use "TOTAL AMOUNT: $100.00". Three separate patterns. - GBP detection runs a priority system. Azure puts "Total (including Tax)" in one corner, Google puts "Total Amount" in another. The parser collects every candidate, scores them by type, and picks the highest-priority largest value.
- Date parsing strips ordinal suffixes ("9th March 2025" becomes "9 March 2025") and tries eight formats. If none match, it falls back to parsing DD-MM from the filename.
- Vendor detection is a hardcoded keyword dictionary of 13 names. If nothing matches, it uses the filename.
The fix pattern, whenever a new layout arrives, is to point Claude at the PDF and the parser and ask it to extend the regex priorities without breaking the existing ones. It takes about ten minutes and a test run against the old invoices to confirm nothing shifted. There's still a logging.warning line in production from when I was last debugging it. That's the tell.
I've deliberately not reached for an LLM to do the extraction. Regex is boring, fast, and free, and when it's wrong it's wrong in an obvious way.
What it replaced, and what it gave back
The two hours a week of data entry is now closer to ten minutes, and most of that is uploading statements. The £500-a-year accountant is gone. More than the money, I know the tax number in real time, which changes how I think about taking on an extra piece of work in February.
The less obvious win: every part of my money lives in one place. Personal, business, and crypto in one database means questions like "how much did I actually spend on AI tooling last year" or "how much unrealised gain is sitting in SOL" take one query, not an afternoon.
What's next
A couple of things I'd do differently and will get to:
- The frontend is one 4,818-line
app.js. It works, but it's the first place I'll feel the pain when I add the next feature. Splitting it into modules is overdue. - Live bank feeds via Open Banking would remove the monthly statement upload step. The reason I haven't is that I like the ceremony of it. Importing is when I actually look at the numbers.
- A second screen for forecasting rather than tracking. The data is all there, the views aren't.
If you're a solopreneur with finances in four spreadsheets and an accountant bill at year end, a weekend of FastAPI and SQLite can genuinely replace it. You just have to be willing to encode your own tax rules.
ctrlaltautomate.com
Top comments (0)