This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 3.
What I Built
Reviewing a bank statement properly means answering three questions: where did the money go, is anything in here wrong, and how is this month going to end. Doing that by hand in a spreadsheet is slow and boring, which is exactly why it gets postponed.
lazy-ledger answers those three questions. Export the statement, run one command, read one page:
- Where it went: spending by category this month next to your 3-month average.
- Worth a second look: a charge far above what you usually pay that merchant, a merchant you have never paid before, a subscription billed twice.
- Where the month is heading: spend so far and the pace to month end.
It runs entirely on your laptop. Gemma 3 4B does the one part that needs language understanding: turning merchant strings like BOLT.EU/O/2610 or NIKORA 2 SABURTALO into categories. Everything that involves money arithmetic is plain Python.
Demo
The demo uses a synthetic statement: six months of everyday spending in Tbilisi, in GEL, generated by make_demo_data.py, with three planted problems: a 410 GEL food-delivery order, Netflix charged twice, and an unknown merchant. lazy-ledger finds all three. The first run labels 31 merchants in about 2.5 minutes on a 2-core CPU with no GPU. The next run sends zero merchants to the model and finishes in about 30 seconds.
Code
Erehemoc
/
lazy-ledger
Bank-statement CSV to a one-page monthly money report, fully offline with Gemma 3 (llama.cpp).
lazy-ledger
Drop in a bank-statement CSV, get back a one-page monthly money report. Runs fully offline with Gemma 3 on your own machine: no cloud, no API keys, your transactions never leave your laptop.
The report tells you three things:
- Where the money went this month, by category, against your 3-month average.
- What's worth a second look: a charge far above what you usually pay that merchant, a merchant you've never paid before, a subscription billed twice.
- Where the month is heading: spend so far and the pace to month end.
Quick start
pip install -r requirements.txt
python fetch_model.py # Gemma 3 4B, ~2.5 GB, one time
python make_demo_data.py # synthetic statement, no real data
python ledger.py demo/statement.csv
cat report.md
With your own statement: export CSV from your bank and run python ledger.py my_statement.csv. Columns are matched loosely: any header containing date, description/merchant/details and amount works…
How I Built It
The model. Gemma 3 4B in GGUF format, run locally through llama-cpp-python. fetch_model.py downloads the same weights ollama pull gemma3:4b would. I started with Gemma 3 1B because it's tiny, but on my test set it called a supermarket "Eating out", so I moved up to 4B.
Where Gemma is used, and where it deliberately isn't:
| Step | Done by |
|---|---|
| Parse CSV (dates, amounts, columns in English, Russian or Georgian) | Python |
| Categorise each new merchant | Gemma, JSON mode, 6 merchants per call |
Remember categories in merchants.json
|
Python |
| Flag spikes, first-time merchants, double billing | Median and MAD per merchant |
| Decide what the summary says | Python |
| Say it in readable sentences | Gemma |
Two things I learned on the way:
- Small models are bad at arithmetic but good at phrasing. My first version let Gemma write the summary from a table of numbers. It told me groceries had "risen" from 1,023 to 979 GEL. So now Python computes the facts ("biggest change: Eating out, 711 GEL, up from a usual 364") and Gemma only rewords them. A guard then checks that every number in Gemma's text was in the facts it was given. If one isn't, the report falls back to the plain sentence.
-
Local knowledge beats a bigger model. Most of the misses at 4B were Tbilisi-specific: Nikora is a supermarket chain, Gorgia sells hardware, Bolt is a taxi app. Three lines of hints in the prompt fixed them. On the demo data it now gets 30 of 31 merchants right. The one miss,
GLOVO*ORDERlabelled Groceries instead of Eating out, is in the README.
Doing less next month. Labels are cached, so only genuinely new merchants reach the model. A wrong label gets fixed once, by editing one line of JSON, and stays fixed.
Why Does Open Innovation Matter?
Because a bank statement is about as personal as data gets. Every line is where you were, what you bought, and when. That shouldn't have to go into a hosted chatbot, and a budgeting app shouldn't need your bank login.
An open-weight model changes that trade. Gemma runs on the laptop you already own, with no account, no API key, and no network after the first download. The cost of a run is a couple of minutes of CPU. And because the weights and the code are open, you can check what happens to your data instead of trusting a privacy policy. If a better small model comes out next month, swapping it in is one --model flag.
The Lazy Payoff
- Before: sort hundreds of transactions into categories by hand, then compare months and hunt for odd charges.
- Now: export the CSV, run one command, read one page. On the demo statement (420 transactions) the first run takes about 2.5 minutes on a 2-core laptop CPU; after that, about 30 seconds, because known merchants are cached.
- The useful part: a double-billed subscription or an odd charge shows up in "Worth a second look" without anyone hunting for it.
Prize Categories
- Gemma: Gemma 3 4B runs locally and does all categorisation and the report summary.
Built with Claude as my coding agent. The demo data is synthetic; no real transactions appear anywhere in the repo.


Top comments (0)