This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle.
What I Built
Quiet Ledger is a receipt and invoice sorter for one-person businesses, and it runs entirely on your own machine.
You drop a PDF invoice, a photo of a receipt, or paste the text. Gemma 3 1B reads it locally, drafts a bookkeeping entry, and shows it to you. You fix anything it got wrong, and then you export a CSV with the columns an accountant actually wants.
That is the whole loop. It exists because I watched a friend who freelances spend the last Sunday of every month with a shoebox of receipts, a bank statement, and a spreadsheet. The work is not hard. It is just relentless, and every cloud tool wants the receipts uploaded to a server first.
Three things make it different from the receipt apps I tried:
- The document never leaves the laptop. No upload, no account, no API key, no per-document fee.
- The model drafts; you approve. This is not a black box that files your taxes. It is a fast first pass you correct in a few seconds.
-
The output is a file you own.
data/receipts.jsonlon disk, and a CSV you can hand to anyone.
It reads the things a freelancer's shoebox actually contains: supermarket receipts, a hardware store tax invoice, a design retainer from another country. It keeps currencies separate instead of quietly adding shillings to dollars, which is the kind of bug that ruins a month.
Demo
The demo is three commands, because the whole point is that it runs where the documents are:
make install && make models && make llama
export LLAMA_BIN="$PWD/.llama.cpp/build/bin/llama-cli"
make run # http://127.0.0.1:8000
There is no hosted demo link, and that is deliberate: hosting it would mean uploading other people's receipts, which is the exact thing this project refuses to do. The repo ships three sample documents so the screenshot above is reproducible from a clean clone.
What a real run looks like. I dropped the sample tax invoice PDF, and 43 seconds later on four CPU cores this came back:
{
"date": "2026-10-12",
"vendor": "KASHA HARDWARE LTD",
"category": "hardware",
"total": 4850.0,
"currency": "KES",
"tax": 620.0,
"payment_method": "mpesa",
"invoice_number": "INV-2026-1044",
"extracted_by": "gemma-3-1b-it"
}
And the export:
date,vendor,category,total,currency,tax,payment_method,invoice_number
2026-10-15,MAMA NJERI'S KITCHEN,meals,1400.00,KES,,cash,
2026-10-12,KASHA HARDWARE LTD,hardware,4850.00,KES,620.00,mpesa,INV-2026-1044
2026-10-09,Northwind Studio LLC,professional-services,2083.20,USD,163.20,bank transfer,NW-8841
Code
https://github.com/mukuvi/quiet-ledger (MIT licensed).
How I Built It
A small FastAPI server, a single-page PWA, and the plumbing from my last offline project, collapsed into one app: Gemma 3 1B with llama.cpp, plus pdftotext and tesseract for getting text out of files.
flowchart LR
A[PDF / photo / paste] --> B[FastAPI]
B --> C[pdftotext or tesseract]
C --> D[Gemma 3 1B via llama.cpp]
D --> E[normalise + sanity checks]
E --> F[you approve or fix]
F --> G[(data/receipts.jsonl)]
G --> H[CSV for the accountant]
The model gets a strict, JSON-only prompt at temperature 0. No chat, no markdown, no commentary, and an explicit instruction to return null rather than invent a value:
- Return JSON only. No markdown, no explanation, no commentary.
- Use null when a value is not printed on the receipt. Never invent values.
- "total" is the final amount paid. If the receipt shows both a subtotal
and a total or amount due, use the final amount due, never the subtotal.
- "tax" is the tax or VAT amount in money, not the tax rate.
That last pair of rules exists because I watched the model get both wrong.
The normaliser is where the real work is. A 1B model is a good reader and a mediocre accountant. Three failure modes showed up in my own test runs, and each one is now handled in code rather than papered over:
- It returned
16%, the VAT rate, in thetaxfield. Money and rates are now parsed differently, and a tax amount smaller than one percent of the total is treated as a rate and replaced with the printed tax line. - It returned the subtotal as the total on an invoice that also had an
Amount dueline. The printed amount due now wins over the model's answer. - It labelled a hardware purchase
other. When the model has no useful opinion, a keyword pass over the receipt text gets a second chance, sohardwarewins overother.
Everything else is deliberately boring. Amounts lose their symbols and separators, dates become ISO, categories map onto a fixed list of twelve, and a payment method of M-PESA becomes mpesa so it does not split into three spellings. Files are deduplicated by SHA-256, so dragging the same receipt in twice does nothing.
Storage is JSON Lines. Not Postgres, not SQLite, not a vector database. A freelancer has a few thousand receipts, not a data warehouse, and a plain file is easier to back up, grep, and explain.
If no model is on disk, the app still runs and labels every entry fallback-parser. I did that on purpose: someone should be able to clone the repo, see the workflow, and decide whether the model is worth the download before committing to it.
Who Would Pay For This
The person I built it for is a freelancer or a two-person studio with client receipts in three currencies and an accountant who charges by the hour to sort them.
The pitch is not "another subscription". It is:
- Privacy that is real, not a policy page. Client names, supplier prices, and where you were on a Tuesday never leave the machine. For anyone under a confidentiality clause, that is the product.
- Zero marginal cost. No per-receipt pricing and no seat count. The download is 0.8 GB, once.
- It works where the work happens. A market trader with a laptop and no reliable connection can sort receipts in the evening. A cloud tool cannot.
- The accountant gets the same CSV either way. The value is the hour back, not a new format to learn.
The honest price for this is low, and that is fine. A tool that costs nothing to run can be priced at what the saved time is worth, and the model can be swapped for a bigger one later without renegotiating a contract.
Why Does Open Innovation Matter?
Every part of this that matters is open, and each one is a practical advantage rather than a slogan.
The document stays private because inference is local. A receipt is one of the most sensitive documents a small business owns. A closed sorter has to move it to a server to read it. Running the weights on the same laptop means there is no trust boundary to audit, because there is no boundary.
It runs with no network. The model, the prompt, and the parser are all on disk. The app is a PWA that opens offline. In a place where connectivity is metered or missing, the open version is not a worse version of the cloud one. It is the only version that works.
I can change the accountant. Gemma runs from a local GGUF file, so moving to a larger Gemma, a different quantisation, or a different model entirely is an environment variable, not a vendor conversation. A regional list of vendor names and tax labels is a fine-tuning run away. That kind of adaptation is impossible when the model is someone else's endpoint.
I can inspect the reasoning. When the model put a tax rate in the tax field, I could read the prompt, change it, and re-run the same document. With a closed API I would be guessing at what changed between two versions.
The cost stays at zero. No API key, no rate limit, no bill that grows with how diligent you are about keeping receipts. That matters most for the people who most need the tool.
The open pieces are not a licensing detail here. They are the reason the product can promise privacy, offline use, and a price of nothing.
Prize Categories
-
Best Use of Gemma: Gemma 3 1B runs locally through
llama.cppand does every extraction in the app. Its output is then validated by the normaliser, which is the part that makes a 1B model trustworthy enough for bookkeeping.
Built for Hacktoberfest 2026. No API key, no upload, no outbound calls when you sort a receipt.

Top comments (0)