This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
TL;DR â Photograph one page of a shop's handwritten credit notebook. An open-weight model reads it (a deterministic parser takes over when there is no key), the shopkeeper confirms every line, the balances follow, and each customer gets a polite WhatsApp reminder in their own language. The AI does the reading; the shopkeeper does the deciding. Repo: https://github.com/ramantiw45/UdhaarBook
What I built
Every kirana, medical and vegetable shop around me runs on a khata â a paper notebook where credit gets written in pen, in a hurry, at the counter. "Ramesh â cheeni 2kg + tel â 340." Nothing is typed anywhere. When money comes back, it's squeezed into the margin in a different pen, and the running balance lives in the shopkeeper's head until the day it doesn't.
The money isn't lost because customers are dishonest. It's lost because asking is awkward. Nobody wants to walk up to a neighbour and say "aapke âš140 baaki hain" â so the shopkeeper waits, the page gets older, and by month end he genuinely isn't sure who owes what any more.
Udhaar Book does three things:
- Reads one page of the notebook from a photo. Local OCR if it's installed, an open-weight model otherwise, a plain deterministic parser if there is no key at all.
- Shows every line for confirmation. Nothing is written to the ledger until a human says it's right.
- Turns balances into reminders, in English, ā¤šā¤ŋā¤ā¤ĻāĨ or ā¤Žā¤°ā¤žā¤ āĨ â with the figure coming from the ledger, never from the model.
Who would pay for this
This is the part I took seriously, because "Side Hustle" invites you to hand-wave it.
A shop carrying âš40,000 of udhaar with even 5% quiet leakage loses âš2,000 every month to money it has already earned. Recovering a third of that is ~âš650 a month. The app costs âš0 to run: free-tier model lanes, one SQLite file, one Android phone that is already on the counter. Charge âš99/month and the tool has paid for itself the first time it recovers a single âš140 line.
The competitor is not another app â it's the pocket of khata apps that want the shopkeeper to type every entry. That's the same reason the money gets lost in the first place: the entry never gets made. The only input that already exists in every one of those shops is the photo of a page.
Demo
![]() |
![]() |
| Read â photo the page, or tick "use the sample page" and walk the whole flow with nothing installed. | Confirm â every line the reader found, editable, with the raw text one tap away. |
![]() |
![]() |
| The book â the ledger: what was given, what came back, what is left, per page. | Who to ask â the reminder screen. Names stay coded until you tap; the message is already written. |
One page of the sample notebook: 12 transactions, 8 customers, âš3,210 given, âš1,000 received, âš2,210 still out. Three of those eight have both taken credit and paid some back â that's the state a shop actually lives in, and the one a hand-written margin is worst at tracking.
Why open matters for what I built
The shop's record of who owes what, and to whom is about as private as a small business gets. Names, amounts, neighbourhood relationships. That record should not require an account, a subscription, or an API that reads it.
-
Reading:
gemma-4-26b-a4b-it(open weights) via Google AI Studio's free tier,qwen/qwen3.8-27bon Groq as failover,gemma3:4bvia Ollama when the shop's own machine can carry it, and a deterministic parser with no key and no network as the floor. -
Swapping is one env var. Ollama and Groq speak plain chat-completions; Gemma 4 on AI Studio has a small native-route adapter (it is no longer served on the OpenAI-compatible endpoint). Pointing
OLLAMA_BASE_URLat a local model still moves the whole pipeline on-device with zero code changes. Nobody can re-price a shop's own bookkeeping. - Cost structure: at âš0 per page, the âš99/month price is a business. Per-token billing turns every read into a cost of goods, and at a âš99 price point that math gets thin fast.
- Data shape: one SQLite file. It can be copied, backed up, deleted, or never uploaded at all. There is no vendor in the middle of a shop's ledger.
The honest asterisk: the model only writes the sentence, never the number. The amount in a reminder is read from the ledger; the model gets to soften the wording and nothing else. That rule is in the code, not the prompt.
One more thing, because "runs locally" is the easiest claim in a hackathon post to fake: the repo ships docs/offline-proof.md, a five-step test (install the reader, switch the Wi-Fi off physically, submit a page) and a table of what each possible outcome lets you claim â including the claims you are never allowed to write, like "100% offline" after a hosted lane answered the request. On this machine the honest row is the third one, and that is the row I used.
How it works
one photo of a khata page
â
âŧ
local OCR (optional, CPU only) âââē raw text
â no OCR installed: the photo goes straight to the lane
âŧ
open-weight lane chain
gemma3:4b Ollama, on-device, offline
gemma 4 26B A4B AI Studio, free tier (native generateContent)
qwen 3.8 Groq, failover
deterministic no key, no network â text only
â
âŧ
transaction rows âââē shopkeeper confirms âââē one SQLite file
â
âââââââââââââââ´ââââââââââââââ
âŧ âŧ
balances per customer WhatsApp reminder
en / ā¤šā¤ŋā¤ā¤ĻāĨ / ā¤Žā¤°ā¤žā¤ āĨ
-
Stack: FastAPI + Jinja2 + SQLite +
httpx. No model SDKs, so a new lane is a dozen lines and a Python upgrade can't break the app on a Tuesday. Fonts are self-hosted; nothing on any screen makes a third-party request. -
The reminder is a link, not a send.
wa.me/?text=...opens WhatsApp with the message written. Nothing leaves the server, no customer phone numbers are stored, and the shopkeeper still chooses who gets it.
The decision that took the longest
I am not going to pretend this was an OCR problem. Reading a messy page is the easy half.
The hard half is that this screen lives on a counter where other people can see it. The reminder screen is a list of the shop's debtors, and a shopkeeper will not use a tool that flashes a neighbour's name at whoever is standing there.
The screen I designed for is 9pm: shutter half down, one tube light, the shopkeeper alone with the notebook and a phone. That is the one moment the names are safe to show â and the moment the screen has to be worth opening.
So the names are coded: each customer shows as an entry number (their place in the order to ask), a hue strip and an amount, and one tap decodes the row into the name, the plain-language history ("Page 1 ¡ 1 line ¡ last 15/10 ¡ âš560 given") and the message. The first row is decoded on load, so the screen teaches itself in one look. Decoding is a one-tap act on a 68px row, and Reveal opens all eight when the counter is finally empty.
The screen is built as a catalogue sleeve, not a dashboard: one white hairline per customer, the amount as the only headline, identity as a hue strip and a number until you touch it. That is written down as a design system rather than left as taste.
Two more things I made the design carry its weight on:
- Nothing is signalled by colour alone. A filled strip means credit was given; a hollow strip with a line through it means money came back. Strip the colour off and the meaning is still there â which also matters because a shopkeeper will screenshot this and print it.
- The reminder screen must survive a greyscale print, so I checked it in greyscale, not just in the browser. It does.
What I got wrong
An honest build log, because the flaws are more useful to read than the features.
-
My machine cannot install the OCR layer. Python 3.14 has no wheels for the usual on-device runtimes yet, so on this box the photo path is: no local reader â model lane. With an AI Studio key set, an uploaded test khata page went through the Gemma lane and came back as rows in the confirm table; with no key, the screen says exactly that and hands you the sample page. The local OCR path is written, documented in
requirements-ocr.txtand untested here. That is a gap, not a feature, and I've labelled it one in the README. - No shopkeeper has used this yet. No field test, no first user, no one has paid me âš99. The sample notebook is a fixture I wrote; the shop name in the screenshots is a fixture shop. I would rather say that plainly than dress up a demo as a deployment.
- Handwriting is genuinely hard, and the honest move is to say so. A benchmark of four open-source OCR engines on 5,578 handwritten prescriptions found the best of them get one word in three exactly right. I read that before building and it changed the design: the app does not try to be right, it tries to be checkable. Every figure is one tap from the raw text the reader saw, and the ledger never accepts a row nobody confirmed.
- Devanagari handwriting is the weakest case for every reader I looked at. The reminder templates are in Marathi and Hindi because the message is the part that must feel native, but I am not going to claim the reading is equally strong in all three scripts.
Also worth reading, if you are building in this space
Three posts that shaped this build:
- I Built a Receipt Bookkeeper That Runs AI Fully Offline â the closest project to this one this week, and the cleanest statement of why the economics only close with open weights. Different user: their freelancer has a wallet of printed receipts; mine has a paper notebook and no laptop.
- How Well Can OCR Read Doctor Handwriting in 2026? â the 1-in-3-words number above, measured rather than assumed.
- Building for the Next Billion Users: Engineering for Emerging Markets â why every screen here is phone-first and why a dead end with no key would have been the real failure.
Prize categories
Best Use of Gemma. Gemma 4 is the default hosted reader (gemma-4-26b-a4b-it, the fast MoE variant, via AI Studio's free tier on the native generateContent route â the OpenAI-compatible one stopped serving Gemma after 3 was retired in 2026), and Gemma 3 4B (gemma3:4b via Ollama) is the on-device lane. The two Gemma lanes sit at the two ends of the price/privacy trade-off â one free and remote, one free and offline â and both are selected by environment variable, not by code.
Run it yourself
git clone https://github.com/ramantiw45/UdhaarBook.git && cd UdhaarBook
python -m venv .venv && .venv\Scripts\activate # or source .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --reload # http://127.0.0.1:8000
No key needed to judge it. Tick use the sample page, press Read this page, and the whole flow runs on the built-in parser: 12 transactions, 8 customers, âš2,210 outstanding, and a Marathi reminder at the end. Add one free AI Studio key in .env and the same flow goes through Gemma; add OLLAMA_BASE_URL and it never leaves the machine. pip install -r requirements-ocr.txt adds the on-device photo reader where wheels exist.
Tests: python tests/test_pipeline.py (5 checks â parsing, money maths, reminders, the no-key path, and the native Gemma request) and python tools/smoke_http.py (20 checks against the running app, including "an uploaded photo fails honestly, with 200, not a blank page"). Neither needs a test framework installed.
Credits
Built with an AI coding agent in an opencode session over one weekend. The product truth was the human's â the shopkeeper, the counter scene, the rule that names stay coded â and the visual direction was chosen from options the agent dealt and built; the agent wrote the app, the test suite, the design system and this draft, and the human approved each ship decision. FastAPI, Jinja2, SQLite, httpx, the self-hosted Rajdhani, Mukta and Spline Sans Mono (all SIL OFL), and the open weights of Gemma 4, Gemma 3 and Qwen 3.8. The sample notebook is synthetic and labelled as such in the repo.
AI disclosure: some_ai.





Top comments (0)