The problem
Every month, small shop owners and freelancers sit down with a pile of bills and type them into a spreadsheet so their accountant can file GST. It's slow, and the bills contain GSTINs, bank details and customer names. Uploading them to a cloud OCR service is a bad trade for something this sensitive.
So I built BillBox for my relative, who keeps a drawer full of paper bills and hands them to the accountant at the end of every quarter.
What it does
Drop in photos or PDFs of bills. A vision model running on your own laptop reads each one and extracts the vendor, GSTIN, invoice number, date and totals. A validation layer checks the math and flags anything suspicious. You fix flagged fields next to the original image, approve, and export a CSV your accountant can open in Excel.
- Demo (sample data, no model runs here): https://billbox-pi.vercel.app/
- Code: https://github.com/amangithub01/billbox
- Real app: runs locally with Ollama (setup in the README)
Why open models mattered
- Privacy: The bill never leaves the machine, because the model runs locally through Ollama.
- Cost: there's no per-page fee, so 500 bills cost the same as 5. That's what makes this viable for someone who earns little from it.
-
Control: the model is one config line (
qwen2.5vl:3bvia Ollama). A bigger or fine-tuned model drops in without changing the app.
My laptop has 8 GB RAM and no GPU, so I used the 3B model. That limits accuracy, and the next section is about what that meant in practice.
What went wrong (and what I did about it)
The model is right-ish, not right. My first test was an electricity bill. It returned valid JSON, but the date came back as 04-07-2018 instead of 2018-07-04, and the total came back as a string, "1031.00", instead of a number. A spreadsheet can't use either. I added a normalization step that converts dates (day-first, the Indian format) and parses numbers like "1,031.00" and "Rs. 1031", with unit tests for each case.
Small models make mistakes on digits, so I stopped trusting the output. Instead of showing extracted values as facts, BillBox runs checks: GSTIN format and checksum, rate x quantity = amount, subtotal + taxes = total, and CGST/SGST vs IGST. A mismatch turns the field red with a plain-English message. On a bill with no GSTIN, BillBox flagged "Missing Vendor GSTIN" and sent it to Needs Review instead of guessing.
Context limits. Sending two images in one request crashed Ollama with a "request exceeds the available context size" error. The app now sends one image per request, resizes to 1280px, and sets a larger context window.
Not every bill is an invoice. My electricity bill has no line items and no GSTIN, so the GST checks flagged errors that weren't errors. Now the checks are skipped when those fields don't exist.
Results
Tested on 10 real bills (printed invoices, phone photos, one electricity bill), on an 8 GB Windows laptop with no GPU.
| Field | Correct |
|---|---|
| Vendor | 10/10 |
| Invoice number | 8/10 |
| Date | 6/10 |
| Total | 8/10 |
About 55 seconds per bill. Validation flagged 4 wrong values that I would otherwise have missed. The worst results were on blurry phone photos and bills with no GSTIN printed on them.
Limitations
- A 3B model needs human review. Treat the output as a draft.
- Roughly a minute per bill without a GPU.
- The hosted demo is simulated. The real extraction only runs locally.
- The CSV is meant for an accountant to open or import. It is not a ready-made Tally import file.
What's next
Fine-tune on the corrections people make (every fix is a labeled example), add Tally-compatible export, and try a larger model on better hardware.
Built with Qwen2.5-VL, Ollama, FastAPI, React and OpenCV.

Top comments (0)