<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sergei Berets</title>
    <description>The latest articles on DEV Community by Sergei Berets (@erehemoc).</description>
    <link>https://dev.to/erehemoc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4202572%2F3c1f202b-bf10-40e8-8597-a27357bade4f.png</url>
      <title>DEV Community: Sergei Berets</title>
      <link>https://dev.to/erehemoc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erehemoc"/>
    <language>en</language>
    <item>
      <title>lazy-ledger: a bank statement turns into a one-page money report, offline, with Gemma</title>
      <dc:creator>Sergei Berets</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:32:02 +0000</pubDate>
      <link>https://dev.to/erehemoc/lazy-ledger-a-bank-statement-turns-into-a-one-page-money-report-offline-with-gemma-5g06</link>
      <guid>https://dev.to/erehemoc/lazy-ledger-a-bank-statement-turns-into-a-one-page-money-report-offline-with-gemma-5g06</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge: Week 3&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lazy-ledger&lt;/strong&gt; answers those three questions. Export the statement, run one command, read one page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where it went:&lt;/strong&gt; spending by category this month next to your 3-month average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worth a second look:&lt;/strong&gt; a charge far above what you usually pay that merchant, a merchant you have never paid before, a subscription billed twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the month is heading:&lt;/strong&gt; spend so far and the pace to month end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs entirely on your laptop. Gemma 3 4B does the one part that needs language understanding: turning merchant strings like &lt;code&gt;BOLT.EU/O/2610&lt;/code&gt; or &lt;code&gt;NIKORA 2 SABURTALO&lt;/code&gt; into categories. Everything that involves money arithmetic is plain Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feipizj8izgbe0rm5e9qm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feipizj8izgbe0rm5e9qm.gif" alt="lazy-ledger demo" width="760" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo uses a &lt;strong&gt;synthetic&lt;/strong&gt; statement: six months of everyday spending in Tbilisi, in GEL, generated by &lt;code&gt;make_demo_data.py&lt;/code&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Erehemoc" rel="noopener noreferrer"&gt;
        Erehemoc
      &lt;/a&gt; / &lt;a href="https://github.com/Erehemoc/lazy-ledger" rel="noopener noreferrer"&gt;
        lazy-ledger
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Bank-statement CSV to a one-page monthly money report, fully offline with Gemma 3 (llama.cpp).
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;lazy-ledger&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Drop in a bank-statement CSV, get back a one-page monthly money report. Runs fully offline with &lt;strong&gt;Gemma 3&lt;/strong&gt; on your own machine: no cloud, no API keys, your transactions never leave your laptop.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Erehemoc/lazy-ledger/demo/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FErehemoc%2Flazy-ledger%2FHEAD%2Fdemo%2Fdemo.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The report tells you three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where the money went&lt;/strong&gt; this month, by category, against your 3-month average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's worth a second look&lt;/strong&gt;: a charge far above what you usually pay that merchant, a merchant you've never paid before, a subscription billed twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the month is heading&lt;/strong&gt;: spend so far and the pace to month end.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install -r requirements.txt
python fetch_model.py              &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Gemma 3 4B, ~2.5 GB, one time&lt;/span&gt;
python make_demo_data.py           &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; synthetic statement, no real data&lt;/span&gt;
python ledger.py demo/statement.csv
cat report.md&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;With your own statement: export CSV from your bank and run &lt;code&gt;python ledger.py my_statement.csv&lt;/code&gt;. Columns are matched loosely: any header containing &lt;em&gt;date&lt;/em&gt;, &lt;em&gt;description/merchant/details&lt;/em&gt; and &lt;em&gt;amount&lt;/em&gt; works…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Erehemoc/lazy-ledger" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model.&lt;/strong&gt; Gemma 3 4B in GGUF format, run locally through &lt;code&gt;llama-cpp-python&lt;/code&gt;. &lt;code&gt;fetch_model.py&lt;/code&gt; downloads the same weights &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Gemma is used, and where it deliberately isn't:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Done by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parse CSV (dates, amounts, columns in English, Russian or Georgian)&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Categorise each &lt;em&gt;new&lt;/em&gt; merchant&lt;/td&gt;
&lt;td&gt;Gemma, JSON mode, 6 merchants per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember categories in &lt;code&gt;merchants.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flag spikes, first-time merchants, double billing&lt;/td&gt;
&lt;td&gt;Median and MAD per merchant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decide what the summary says&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Say it in readable sentences&lt;/td&gt;
&lt;td&gt;Gemma&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two things I learned on the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Small models are bad at arithmetic but good at phrasing.&lt;/em&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Local knowledge beats a bigger model.&lt;/em&gt; 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, &lt;code&gt;GLOVO*ORDER&lt;/code&gt; labelled Groceries instead of Eating out, is in the README.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Doing less next month.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;--model&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Before: sort hundreds of transactions into categories by hand, then compare months and hunt for odd charges.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;The useful part: a double-billed subscription or an odd charge shows up in "Worth a second look" without anyone hunting for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma&lt;/strong&gt;: Gemma 3 4B runs locally and does all categorisation and the report summary.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with Claude as my coding agent. The demo data is synthetic; no real transactions appear anywhere in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
