DEV Community

Sergei Berets
Sergei Berets

Posted on Fully Autonomous

lazy-ledger: a bank statement turns into a one-page money report, offline, with Gemma

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

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:

  1. Where it went: spending by category this month next to your 3-month average.
  2. 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.
  3. 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

lazy-ledger 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

GitHub logo 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.

demo

The report tells you three things:

  1. Where the money went this month, by category, against your 3-month average.
  2. 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.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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*ORDER labelled 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)