<?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: musafirilmu21-max</title>
    <description>The latest articles on DEV Community by musafirilmu21-max (@ririchyou).</description>
    <link>https://dev.to/ririchyou</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%2F4190819%2Fb6e15084-6263-424c-8bd8-4e0200c71ff2.png</url>
      <title>DEV Community: musafirilmu21-max</title>
      <link>https://dev.to/ririchyou</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ririchyou"/>
    <language>en</language>
    <item>
      <title>I built a private bookkeeper you can talk to — and learned where a small local model actually earns its keep</title>
      <dc:creator>musafirilmu21-max</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:41:35 +0000</pubDate>
      <link>https://dev.to/ririchyou/i-built-a-private-bookkeeper-you-can-talk-to-and-learned-where-a-small-local-model-actually-earns-56k6</link>
      <guid>https://dev.to/ririchyou/i-built-a-private-bookkeeper-you-can-talk-to-and-learned-where-a-small-local-model-actually-earns-56k6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every freelancer knows the tax-time ritual: scroll a year of bank lines, tag each one, and try to remember whether &lt;code&gt;SQ *BLUE BOTTLE 4471&lt;/code&gt; was a client meeting or a personal coffee. The obvious "AI solution" is to upload your whole financial life to a SaaS and let their model sort it. I wanted the opposite: &lt;strong&gt;do it on your own laptop, offline, and use the model only for the part a model is actually good at.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Deductible&lt;/strong&gt; — a local-first bookkeeping copilot. It tags your transactions into 10 expense categories, applies the right tax treatment, exports a tax-prep starter CSV (categorised spending + deductible estimates) your accountant can drop into Schedule C, and then lets you &lt;em&gt;ask your books questions in plain English&lt;/em&gt; — with no data ever leaving the laptop.&lt;/p&gt;

&lt;p&gt;The classifier is a &lt;strong&gt;hybrid&lt;/strong&gt;: deterministic rules handle merchants you have seen before (fast, free, exact), and an open-weight LLM handles the unknown long tail — the &lt;code&gt;SQ *BLUE BOTTLE 4471&lt;/code&gt; garbage that makes keyword tables rot. I benchmarked it three ways and got two answers that disagree: on the 29 transactions I tuned the rules against, the model added &lt;strong&gt;nothing&lt;/strong&gt;. On 150 merchants I deliberately kept out of the keyword table, the rules collapsed to 23% and the hybrid reached 49%. Both numbers are in here — along with the one category where the model is simply blind, and the awkward detail that the hybrid's win over the &lt;em&gt;rules&lt;/em&gt; is decisive (p&amp;lt;0.001) while its win over the &lt;em&gt;model alone&lt;/em&gt; is statistically nothing (49% vs 46%, p=0.23).&lt;/p&gt;

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

&lt;p&gt;Two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Categorize&lt;/strong&gt; a bank/card CSV into 10 expense categories, apply tax treatment (meals 50%, personal 0%, the rest 100%), total it up, and flag items worth a human glance. Output is an auditable CSV — the numbers come from plain Python, not from a model's vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain.&lt;/strong&gt; &lt;code&gt;ask.py&lt;/code&gt; builds a &lt;em&gt;deterministic&lt;/em&gt; snapshot of your finances and lets the local model answer questions like &lt;em&gt;"What are my biggest deductible expenses, and what should I double-check before filing?"&lt;/em&gt; — offline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything runs on an open-weight model (Qwen2.5 via &lt;code&gt;llama-cpp-python&lt;/code&gt;, GGUF): no network, no API key, no per-row cost. &lt;code&gt;--mode hybrid&lt;/code&gt; is the default; &lt;code&gt;--mode dry&lt;/code&gt; needs no model at all.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python deductible.py &lt;span class="nt"&gt;--csv&lt;/span&gt; sample_data/transactions.csv &lt;span class="nt"&gt;--out&lt;/span&gt; taxes_ready.csv
python ask.py &lt;span class="nt"&gt;--csv&lt;/span&gt; taxes_ready.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--question&lt;/span&gt; &lt;span class="s2"&gt;"What are my biggest deductible expenses, and what should I double-check before filing?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ask layer prints a snapshot and then the model's answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Headline facts (precomputed):
- Total estimated deductible: $3,381.00
- Deductible category #1: Equipment = $1,999.00
- Deductible category #2: Professional Services = $450.00
- Deductible category #3: Travel = $371.60
- Top 3 categories combined: $2,820.60

Flagged for review (possible misclassification or large amount):
- MACBOOK PRO 14 M4: $1,999.00 -&amp;gt; Equipment
- DOCTOR VISIT COPAY: $40.00 -&amp;gt; Other/Personal
- NETFLIX SUBSCRIPTION: $15.49 -&amp;gt; Other/Personal
- WHOLE FOODS GROCERIES: $134.22 -&amp;gt; Other/Personal

ANSWER
--------
The biggest deductible expenses are in the Equipment category, totaling
$1,999.00. This should be double-checked to ensure it is correctly classified as
equipment rather than other personal expenses. The Professional Services
category, at $450.00, is also a significant expense that should be verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(That run: &lt;code&gt;sample_data/transactions.csv&lt;/code&gt;, &lt;code&gt;--mode hybrid&lt;/code&gt;, &lt;code&gt;qwen2.5-1.5b q4_k_m&lt;/code&gt;, pasted verbatim — all four flags, no trimming. Note what did **not&lt;/em&gt;* happen: on this clean set every row matched a keyword rule, so the model never fired. That is the hybrid working as designed, and it is why this run takes under a second. The messy set below is where the model actually earns its keep; the screenshot uses &lt;code&gt;transactions_messy.csv&lt;/code&gt;, which is why its total differs.)*&lt;/p&gt;

&lt;p&gt;Note the design choice: &lt;strong&gt;the model never does the math.&lt;/strong&gt; The snapshot is precomputed by auditable Python; the model only explains and advises. That is the difference between a toy and a tool your accountant does not have to re-check from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  See it running
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxna4m686jddl3hoqgrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frxna4m686jddl3hoqgrm.png" alt="Deductible ledger UI: a messy bank statement, an offline answer, and a categorized ledger" width="800" height="1295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is a real run of the browser UI (&lt;code&gt;web.py&lt;/code&gt;), captured by an automated Playwright test (&lt;code&gt;e2e_web_test.py&lt;/code&gt;) that loads the page, asks a question, and checks the rendered ledger and answer — so the screenshot is reproducible, not hand-typed.&lt;/p&gt;

&lt;p&gt;The honest detail: this is the hybrid run, and the model got &lt;code&gt;FACEBK&lt;/code&gt; right but &lt;code&gt;INSTACART&lt;/code&gt; wrong (groceries classified as Software). That is exactly why every model-classified row now lands in the “Review these” box and the total is labelled an estimate. A clean ledger with no flags would be a prettier lie.&lt;/p&gt;

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

&lt;p&gt;GitHub: &lt;a href="https://github.com/musafirilmu21-max/deductible" rel="noopener noreferrer"&gt;https://github.com/musafirilmu21-max/deductible&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deductible.py&lt;/code&gt; — categorizer (&lt;code&gt;--mode hybrid|model|dry&lt;/code&gt;) + CSV export&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ask.py&lt;/code&gt; — the "talk to your books" layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;benchmark.py&lt;/code&gt; — the small-set accuracy table above&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;benchmark_heldout.py&lt;/code&gt; — the 150-merchant held-out set (the numbers that matter)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;web.py&lt;/code&gt; — a local browser UI (the screenshot above)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e2e_web_test.py&lt;/code&gt; — Playwright test that drives the UI end to end&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test_deductible.py&lt;/code&gt; — 13 tests for the deterministic parts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; — CI runs the tests and the keyword benchmark on every push
(&lt;a href="https://github.com/musafirilmu21-max/deductible/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;badge&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Sample data + a clean MIT license.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The interesting part is what the benchmark forced me to change.&lt;/p&gt;

&lt;p&gt;I started with the obvious design: the model classifies each transaction. To make a tiny model reliable I constrained its output to a single number 1–10 and gave it one few-shot example per category. Then I measured — honestly, including the sets where my own design lost.&lt;/p&gt;

&lt;p&gt;Two uncomfortable truths fell out first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A hand-tuned keyword table beats the models on clean names.&lt;/strong&gt; Of course it does — I wrote the keywords against those exact names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bigger was not automatically better&lt;/strong&gt; — 3B did not beat 1.5B at this task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And on those 29 rows the hybrid tied the keyword baseline exactly (15/15 clean, 13/14 messy). On a set I had tuned against, that is the honest result: no detectable model contribution. So I built a &lt;strong&gt;held-out set of 150 merchants I deliberately did &lt;em&gt;not&lt;/em&gt; add to the keyword table&lt;/strong&gt; — the fair test of generalization:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;set&lt;/th&gt;
&lt;th&gt;keyword rules&lt;/th&gt;
&lt;th&gt;Qwen2.5-1.5B&lt;/th&gt;
&lt;th&gt;hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clean names (keywords tuned here)&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;td&gt;11/15&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;messy strings (n=14)&lt;/td&gt;
&lt;td&gt;13/14&lt;/td&gt;
&lt;td&gt;7/14&lt;/td&gt;
&lt;td&gt;13/14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;held-out, 150 unseen merchants&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34/150 (23%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69/150 (46%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74/150 (49%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There the picture changes completely. Unseen merchants fall straight through a keyword table to "Other/Personal", so rules get 23%. The model alone gets 46% by generalizing from the merchant string. The hybrid gets &lt;strong&gt;49% — 2.2× the rules&lt;/strong&gt; — because it keeps rules for the merchants they know and hands only the unknowns to the model.&lt;/p&gt;

&lt;p&gt;Two things that headline hides, and they are the more interesting findings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules are worse than 23%.&lt;/strong&gt; Only 124 of the 150 rows have no keyword match at all; 11 of the rules' 34 "correct" answers are correct purely because the fallback default (&lt;code&gt;Other/Personal&lt;/code&gt;) happened to be the right label. Strip those out and the keyword table is really making 23 genuine hits, not 34.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model has one total blind spot.&lt;/strong&gt; Every Equipment merchant in the held-out set — Dell, Lenovo, Logitech, Samsung, Sony, Bose, Anker, Wacom, GoPro, DJI, Razer, HP, Asus, Best Buy, Microsoft Store — comes back as &lt;code&gt;Software &amp;amp; Subscriptions&lt;/code&gt;. &lt;strong&gt;0 out of 15.&lt;/strong&gt; That is not a model that generalizes; that is a taxonomy it never learned, and prompt tuning did not fix it. It is also the best argument for the review box: a confident wrong answer on a $2,000 laptop is exactly what you want flagged, never filed.&lt;/p&gt;

&lt;p&gt;And the significance check is unkind in one direction and generous in the other. Hybrid vs rules is decisive (exact McNemar, p&amp;lt;0.001). Hybrid vs the model alone is 74 vs 69 correct — discordant pairs 8 to 3, p=0.23, i.e. &lt;strong&gt;not distinguishable from noise&lt;/strong&gt;. So the honest case for the hybrid is not "more accurate than the model". It is &lt;em&gt;cheaper and faster&lt;/em&gt;: the rules answer the 26 rows they recognise instantly and for free, and the model only runs on the 124 it cannot. On this set that is 23s versus 28s — about 17% faster, measured on CPU, single-threaded. I would rather ship the architecture that is meaningfully faster at statistically identical accuracy and say so, than claim a 3-point win I cannot defend.&lt;/p&gt;

&lt;p&gt;And 49% is still only half right. That is exactly why every model-classified row lands in the "Review these" box: the model is a generalizing first pass, not an oracle. The product is "sort the easy ones and flag the rest for you", not "file your taxes".&lt;/p&gt;

&lt;p&gt;Then I asked the better question: &lt;em&gt;what is a language model actually good at here?&lt;/em&gt; Not memorizing that "Starbucks = meals" — a rule does that for free. It is good at &lt;strong&gt;talking to a human about their money.&lt;/strong&gt; So the model's headline job became &lt;code&gt;ask.py&lt;/code&gt;: turning a spreadsheet into a bookkeeper you can ask &lt;em&gt;"what should I double-check before filing?"&lt;/em&gt; — grounded in numbers Python computed, with the model steered toward the snapshot rather than left to do math (a small model can still slip; there is no output validator yet).&lt;/p&gt;

&lt;p&gt;The architecture, in one line: &lt;strong&gt;rules classify the known, the model guesses the unknown, Python does the math, and a human checks the flags.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honest scope: this is a categorisation aid, not a tax filing. It is the input layer to a real Schedule C — it does not do home-office Form 8829, a vehicle log, or depreciation. Those stay with your accountant. What it saves you is the hour a month spent eyeballing a year of statements into the right buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Side-Hustle Economics (who actually pays)
&lt;/h2&gt;

&lt;p&gt;The buyer is the &lt;strong&gt;solo 1099 contractor&lt;/strong&gt; — a designer, developer, VA, or consultant doing $40–150k/yr. Their pain is real and monthly: a bank export that reads like &lt;code&gt;SQ *BLUE BOTTLE 4471&lt;/code&gt; and a tax bill that punishes a mislabelled meal.&lt;/p&gt;

&lt;p&gt;What would they pay? A freelance accountant charges &lt;strong&gt;$150–400/hr&lt;/strong&gt;; a year-end session alone is several hundred dollars, mostly billed for what Deductible does in minutes. At &lt;strong&gt;$15/quarter&lt;/strong&gt; it only has to remove about ten minutes of a $200/hr accountant hour to pay for itself. The honest pitch is not "replaces your accountant" — it is "stops you paying your accountant to tag coffee." That is a real willingness-to-pay signal, not a land-grab TAM.&lt;/p&gt;

&lt;p&gt;The unit economics work &lt;em&gt;because&lt;/em&gt; it is open weights: $0 marginal inference cost. The honest caveat is the install cliff — today it ships as a Python CLI, so the real first customer is the technical freelancer; the paid version is a signed desktop binary that bundles the model. That packaging, not the model, is the product work left.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This is a one-day build — there is no real user yet, so this is the pricing hypothesis, stated honestly rather than a fake testimonial.)&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is the product.&lt;/strong&gt; Financial data is the most sensitive data a freelancer owns. Running the model locally means it never leaves the laptop — no ToS, no breach surface, no vendor seeing your ledger. A closed API cannot offer that at any price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0 marginal cost.&lt;/strong&gt; Tax season is bursty. An API billed per token turns a free tool into a bill you avoid using in March. A local model costs nothing per run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappability as a feature, not a limitation.&lt;/strong&gt; Open weights let &lt;em&gt;you&lt;/em&gt; choose the brain: a 0.5B for a Raspberry Pi, a 1.5B for a laptop, or a fine-tune on your own ledger for better accuracy — by changing a file path. The benchmark above only exists &lt;em&gt;because&lt;/em&gt; weights are open and downloadable. You cannot A/B three models behind one closed endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest tooling.&lt;/strong&gt; Because I can read and swap the model, I can benchmark it and tell you the truth about where it is weak. That transparency is only possible in the open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;Not yet — this was built in a day. The explicit next step is to hand it to a freelancer for one real quarter of transactions and report back what they used, what they ignored, and what they asked for next. (Early signal from showing the &lt;code&gt;ask.py&lt;/code&gt; flow: people want a way to teach it &lt;em&gt;their&lt;/em&gt; recurring vendors — which is exactly the fine-tune path open weights make possible.)&lt;/p&gt;

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

&lt;p&gt;No partner-specific technology used — built on open-weight Qwen models via &lt;code&gt;llama-cpp-python&lt;/code&gt;. Entering for the &lt;strong&gt;overall prize&lt;/strong&gt; only (no partner category applies).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you got value from this, try &lt;code&gt;benchmark.py&lt;/code&gt; on your own bank export and tell me how your vendors do — I would genuinely like to see where the model breaks.&lt;/em&gt;&lt;/p&gt;

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