Existing trackers ask you to upload your soul.
Every "AI" budgeting app I tried wanted the same thing: full read access to my bank, broker, and credit cards — and a quiet promise that it would live happily on someone else's GPU. The trade was supposedly worth it, because LLMs would sort transactions, flag weird charges, and write mildly judgmental summaries of my coffee habit.
The category I cared about most was the one I trusted least. Transactions are biographical. They show where I sleep, who I live with, what I'm sick from, and what I'm trying to quit. Sending all of that to a third party for a feature I could write in a weekend felt backwards.
So I asked the obvious question: how good can a tracker be if the model never leaves the laptop?
A local model is a feature, not a fallback.
Three rules I refused to break.
-
No outbound calls for inference.
If the laptop is offline, ZenSpend still works. Categorisation, search, and the chat coach all run on a local Llama 3 via Ollama.
-
Plain-text exports must always be possible.
Every transaction is just a row in Postgres. No proprietary format, no lock-in.
pg_dumpis the export button. -
The UI is not allowed to feel like a database admin tool.
Most finance UIs default to grid + sidebar + 200 chart options. ZenSpend is a chat surface with one strong opinion at a time.
Four boxes, one machine.
The system is deliberately small. Each transaction passes through four
stages — ingest, embed, store, retrieve — and the whole pipeline runs
inside a single docker compose up on a M-series MacBook.
The interesting bit is step 03: every transaction stores both a structured row and its embedding. That means "how much do I spend on caffeine in a stressful week?" is one vector search joined to one SQL aggregate — not a retraining job, not a re-prompt, just a hybrid query.
Local models lie differently than hosted ones.
The first painful lesson was that a 7B local model categorises merchants competently but fabricates totals when asked a numeric question. It would happily report I spent £312 on groceries when the real number was £267 — close enough to sound right, wrong enough to ruin the tool.
The fix was unglamorous: never let the model do arithmetic. Numbers come from SQL. The LLM only does three things — classify, summarise, and pick the right query template. Once that boundary was enforced, every accuracy bug disappeared at once.
A coach, not a dashboard.
The biggest design call was rejecting the spreadsheet. Most finance tools open onto a wall of categories, totals, and pie charts. That layout assumes you already know what to ask. I wanted the opposite: one prompt at the top, one chart at the bottom, one piece of advice in between.
The chat coach is therefore the entire app. Charts and tables exist, but they appear inside a conversation as the model decides they're warranted — closer to a teammate explaining your week than a tool you have to operate.
Three things, if I built it again.
-
Ship without the chart kit.
I spent a week wiring Recharts before realising the chat coach answered most questions in a sentence. The dashboard is now the least-used surface.
-
Treat the prompt template as a versioned artefact.
Categorisation behaviour drifted whenever I tweaked the system prompt. Tagging each prompt with a version + a small eval set would have caught regressions weeks earlier.
-
Make "explain this charge" a first-class action.
The single feature people actually used in dogfooding was asking the model "what was this £42 thing on Tuesday?" — and that was hidden three menus deep in the prototype.