Financial Audit
Financial Audit answers one question most finance tools dodge: are you actually on track, or not? You tell it your target net worth, drop in raw statement exports from your bank, broker and pension provider, and about a minute later you get a verdict, a health score out of 100, and a prioritised list of what to fix.

It started life as a dashboard of my own finances, hardcoded in JSX, which was useful for exactly one person. The interesting engineering happened when I turned it into something anyone could upload their statements into. Here's how that went.
Why I don't parse bank statements
The obvious approach to reading a bank PDF is templates: write a parser for Monzo's layout, one for Vanguard's, one for Santander's, and keep patching them every time a bank redesigns a statement. I skipped that entirely. The app extracts the raw text in your browser with pdf.js, no layout logic at all, and hands the text soup to Claude Haiku with a schema: accounts, transactions, holdings, provider, currency. The model does the structural parsing. Spreadsheets get the same treatment, converted to CSV client-side with SheetJS first.
This wasn't the first architecture. Originally the files went to the server, and a 50MB PDF met Vercel's 4.5MB request limit and a 10-second function timeout, and everyone had a bad time. Moving extraction into the browser fixed both at once, because a 50MB PDF becomes about 50KB of text. It also means the statement file itself never leaves your machine. To be precise about the privacy claim, because vague privacy claims are their own kind of con: the binary stays local, but the extracted text does transit the API to be parsed, and nothing is stored anywhere. No accounts, no database.
The 7-year Monzo statement
The best bug came from testing with my own bank history. Seven years of Monzo is thousands of transactions, and Haiku dutifully tried to return all of them as JSON until it hit the output token ceiling and stopped mid-array. Truncated JSON parses as nothing, so one big file killed the whole audit.
The fix has two halves. The prompt now sets a hard budget, at most 200 transactions per account, descriptions under 40 characters, amounts rounded to whole numbers. And behind that there's a little function called repairTruncatedJson that walks the brackets of a cut-off response, snips it back to the last fully-closed element, and closes the brackets itself.
Two more parsing bugs earned permanent fixes. A single Vanguard export turned out to contain an ISA, a GIA and a pension in one file; the schema originally assumed one account per file, so the dashboard quietly showed a fraction of your investments. And the parser once swapped the cost and value columns, which showed users a portfolio worth £0 with a return of -100%. That one is now guarded in three places: the prompt, the server, and the client.
Where the AI is allowed, and where it isn't
The rule I settled on: the model reads, the maths counts. The verdict and the health score are deterministic, the same inputs give the same answer every time. The score is 100 points: 25 for your emergency fund, 20 for debt ratio, 20 for diversification, 35 for goal pace, where goal pace projects your net worth forward at a 7% real return and asks how many years until you hit your target. Fifteen years or fewer gets full marks; if it never happens in sixty, zero.
Claude comes back in exactly once more, for the action plan, and gets a deliberately slimmed payload: totals, balances, holdings and your goals, with the transaction history stripped out. It returns findings with severities and an estimated impact in pounds, because "consider diversifying" is useless and "Bed & ISA £20k of GIA gains this tax year, worth roughly £400" is not.

Subscription detection is also deliberately not AI: a pile of regexes and a rule that a merchant must appear in two different months at a near-identical amount. The tolerance started at ±15% and had to be tightened to ±5% because Uber rides kept qualifying as subscriptions. There is a denylist that now contains, among other things, AEROPUERTO.
The model diet
The action-plan endpoint started on Opus and timed out. Then Sonnet, still timing out on big files. It ended up on Haiku 4.5 with parallel per-file parse calls and a cached system prompt, which took total parse time from the sum of your files to the slowest single file, roughly a 10x speedup on a real multi-file audit. Almost all of that happened in one long day of commits, which is the honest shape of vibe-coded performance work: nothing was slow in testing, everything was slow the moment a real seven-year statement showed up.

The tax tab is pure arithmetic on UK constants, CGT above the £3,000 allowance at 20%, dividends over £500 at 8.75%, and a "years to Bed & ISA your GIA" estimate. No model in sight, because tax is the last place you want a plausible-sounding guess.
It's not finished. Geography is inferred from fund names, the dividend yield is a flat assumption, and one production crash in August (a missing import that the error boundary caught) reminded me why that error boundary exists. But it answers the question it was built for, in about a minute, for the price of two Haiku calls.