跳到正文
DEV Community· Ankith M·· 2 小時前AI 評分46

BillSahi:我開發 GST 檢查工具,店主卻説 GST 並非他的問題

BillSahi: I built a GST checker, and a shopkeeper told me GST wasn't his problem

AI 導讀

BillSahi 開發者因應一名印度小店店主的需求,將 GST 税率核對工具擴展為供應商帳單檢查工具,加入 GST 申領資格核查、價格變動及欠款追蹤。店主將於 2027 年 1 月轉用一般 GST 制度。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

Four bills on a counter near Pavagada

The supermarket is in a village outside Pavagada, in Tumkur district. Like most shops of its size it doesn't buy everything from one wholesaler. Soaps and Horlicks come from the Hindustan Unilever distributor, coconut oil and gram-flour soap from another, detergent from a third, and creams and lotions from whoever brings them that week. When I asked the owner for supplier bills, he handed me what he had kept: four of them.

Two looked like any GST tax invoice. One was printed from a phone app with a running balance in big letters at the top: ₹2,13,672 due. One just said CASH BILL 2025: 31 lines of creams and soaps, ₹15,252 in total, no supplier name, no GSTIN and no tax shown anywhere.

I had come with a GST rate checker. I had spent the week building it: you photograph a supplier's bill, and it checks every line against the official rate. So I asked him what worries him about supplier bills.

It wasn't the GST rate. He has hundreds of suppliers, he said, and most of them charge the same prices for the same goods. A few charge more, a few charge less, and that's what bothers him. The other problem, a big one in his words, is keeping track of credit: who he owes, and how much.

Then he told me the part that made GST matter again. The shop is registered under the composition scheme, so today it can't claim back any GST it pays on purchases. From January 2027 he's moving to regular GST, and from then on every purchase bill has to be good enough to claim the GST back.

So BillSahi changed. This post is about what it does now, and what it found on his four bills.

What BillSahi does now

BillSahi checks a small Indian shop's supplier bills. Take a photo of the bill and it tells you four things:

  1. Is the GST rate right on every line? Every line is checked against the official rate, and any difference is shown in rupees.
  2. Can you claim this GST back? It checks the supplier's GSTIN (format and check digit), your GSTIN on the bill, GST shown on every line, and the HSN and rate on every line, then gives the rupees of credit at risk if something is missing.
  3. Did you pay less for this before? Every line is saved with what one unit cost the shop. When a later bill charges more for the same item, from any supplier, you see it, with the rupees extra on that bill.
  4. How much do you owe each supplier? Every bill is added to that supplier's account; record a payment in one tap and see who you owe, next to what the supplier's own bill says you owe.

It also still does what I first built it for: type items the way a shop writes them ("Parle G 800g", "hawai chappal 199", "ಬಟ್ಟೆ ಸೋಪು 4pc") and get the HSN heading and GST rate.

Who pays, and how much. I asked the owner. He said he would pay, but not much. He buys stock only two or three times a month, so he would use it two or three times a month, and around ₹49 a month is what he said he'd pay. That number shaped the build: at ₹49 a month, every bill check has to cost almost nothing to run, which is why open models matter here (more on that below).

The one rule everything is built around: the model never states a rate. It picks an HSN heading, then one of the official notification entries for that heading, and code reads the rate from the entry. Everything that's arithmetic (rupees overcharged, credit at risk, the ₹2,500 line for clothing and footwear, what one unit cost, what you owe) is done by code.

Demo

What it found on the four real bills

62 lines across four bills from September and October 2026. The photos were forwarded on WhatsApp, so they are compressed. I didn't retake them; this is what a real shop would send.

Bill Lines Read right What BillSahi found
Hair-oil and soap distributor 10 10 of 10 Aloe vera skin gel billed as hair oil (HSN 3305 at 5%). Skin-care gel is 3304 at 18%: GST short by ₹174.57.
Detergent distributor 3 3 of 3 All rates right. Supplier's printed balance ₹2,13,672 against this bill's ₹3,648.
Cash bill 31 31 of 31 No GST shown at all on ₹15,252 of purchases. 30 of 31 items placed correctly; hair colour wasn't.
HUL distributor (dense, blurry photo) 18 partly Rows from line 15 shifted by one. BillSahi caught it and marked nothing (see the failure below).

The credit check is where his January problem shows up. Neither GST invoice has the shop's GSTIN on it. Today that doesn't matter, because a composition shop claims nothing. From January, a bill without his GSTIN means the supplier's GST doesn't show up against his GSTIN, and he can't claim it. On these two bills alone that's ₹834.76 + ₹556.48 = ₹1,391.24 of GST, and the cash bill would add ₹15,252 of purchases with no claimable GST at all. That's why BillSahi now ends every bill with "Can you claim this GST back?" and a list of ✓ and ✗.

The aloe vera catch is the kind of thing nobody checks by hand. The bill prints HSN 33059011 (hair oil, 5% since GST 2.0). The product is a skin gel, which is 3304 at 18%. Here the supplier charged less GST than due, so it's not money the shop lost today. But a wrong HSN on a purchase bill is exactly what an officer questions once the shop starts claiming credit.

The detergent supplier's balance is the credit problem in one number: the bill is for ₹3,648, and the top of it says ₹2,13,672 due. Two weeks after this bill, would the owner know which of those numbers is right? In BillSahi's supplier book, the supplier's printed balance sits next to what the shop's own bills add up to, so a gap is visible instead of buried.

Speed. Each bill took between 3.6 and 7.7 minutes on the free hosted Gemma 4 31B, from photo to every line checked: 3 lines in 217 s, 10 lines in 259 s, 31 lines in 353 s, 18 dense lines in 461 s. Most of that is the vision read and one model call per line. It's slow for a demo and fine for a shop that buys stock three times a month; the page prints each line as soon as it's checked, so nobody stares at a spinner.

Code

GitHub logo Ankith-m1006 / billsahi

Right HSN code and official GST rate for small Indian shops, and a check of every supplier bill for the wrong rate. Open models: Qwen3.5-4B fine-tuned on Tinker, Gemma 4.

BillSahi

Is your supplier's bill sahi? BillSahi checks a small Indian shop's supplier bills: the GST rate on every line, whether the bill is good enough to claim the GST back, whether a supplier is charging more than the shop paid before, and how much the shop owes each supplier.

Live: https://billsahi-pb7k.onrender.com · Demo book with four real bills from a supermarket near Pavagada, Tumkur: https://billsahi-pb7k.onrender.com/?shop=kvr-demo#book

Type items the way a shop writes them (brands, Hinglish, Hindi or Kannada, abbreviations, pack sizes) or upload a photo of a supplier's GST bill. BillSahi finds the 4-digit HSN heading, matches the official GST notification entry, and reads the rate from that entry.

The model never states a rate. It picks a heading and an entry; code reads the rate from the official CBIC notifications. When the rate depends on something the item name doesn't say (pre-packaged or loose, sale value above…

How I Built It

1. The official rates, as data. I parsed every goods entry of CBIC Notification 9/2025-CT(Rate) and the exemption Notification 10/2025-CT(Rate) (GST 2.0, effective 22 September 2025), and applied the later amendments 19/2025 and 01/2026: 1,570 rows covering 1,372 entries, each with its source notification, plus spot checks against the PDF text (data/RATES_PROVENANCE.md).

The thing that surprised me is that an HSN heading often doesn't decide the rate on its own. Heading 3401 holds toilet soap in bars (5%) and other soap and washing preparations (18%). Heading 3304 holds talcum and face powder (5%) and skin-care creams (18%); that split is the aloe vera catch above. So the model's job isn't "guess the code", it's "pick the official entry that describes this item".

2. Search (Tiger Data). The 12,604-code HSN directory and the 1,570 entries live in Postgres on Tiger Data with BM25 indexes (pg_textsearch). Every item first gets a shortlist of headings from plain search. The shop's own book lives in the same database: bills, lines and payments, created on first use.

3. Reading the bill (Gemma 4). Gemma 4 31B reads the photo and only transcribes: supplier and buyer GSTIN, invoice number and date, the bill total, any running balance, and for each line the item, printed HSN, MRP, quantity, taxable value, GST rate and line total. The request is streamed. Before I switched to streaming, the dense HUL bill failed three times in a row with "remote end closed connection": a long, silent request gets dropped as idle, while a streamed one keeps the connection busy.

4. Heading, then entry. For each line, the model picks a heading from the shortlist, then one entry from the official entries for that heading, shown without their rates. Code rejects a heading that isn't in the directory and an entry that isn't in the list, and applies the ₹2,500 threshold for clothes and footwear itself.

Shop bills write things like "PCNO 500ML FT" (Parachute coconut oil) and "MORELIGHT LIQUID 99/-". From the name alone, neither can be placed. When the name fails, BillSahi uses the HSN printed on the bill as a hint and marks the line "read from the bill's HSN". On the real bills that rescued 7 lines: 5 coconut-oil lines and 2 detergent lines. It's used only when the name fails, so a wrong HSN on a named item (like the aloe vera gel) can't hide a wrong rate.

5. Price memory and the supplier book. Plain code (app/ledger.py):

  • What one unit cost: the line total divided by the quantity. For a composition shop, the GST is part of the cost because it can't be claimed back.
  • Same item? The same pack size and MRP when both are printed, and most of the name's words shared. Suppliers spell brands differently: the cash bill says "LIFEBOY RED SOAP 125G", and another bill could say "LIFEBUOY". Near-identical words count as shared. A test against the saved real bills flagged a Lifebuoy line at ₹40 a piece against the ₹38 the shop paid before (₹48 extra on 24 pieces), and left alone a Garnier line whose MRP differed (a different pack) and a Santoor line at the same price.
  • Dues: the bills of a supplier minus the payments recorded, with the supplier's own printed balance shown next to it.
  • Credit check: the GSTIN check digit is a mod-36 checksum over the first 14 characters, so a GSTIN misread from a blurry photo shows up as "check digit doesn't match" instead of passing as valid.

6. A fine-tuned open model (Tinker). Before the shop visit, I fine-tuned Qwen3.5-4B with LoRA on Tinker to answer the same two questions (heading, then entry) as short JSON. The training data was written by open models and checked by another one. Gemma 4 and Qwen3.5-397B (on Tinker) wrote shop-style item names for every notification entry, and every item was classified blind by the other model and dropped if the two disagreed on the chapter (about 76% kept). That caught real label errors, such as a Hep-B test kit filed under the vaccines' chapter. The result was 20,689 training conversations. Data generation and training together used about $10.69 of Tinker credit.

7. Traces (Sentry). Every item is an agent span with tool spans (search, official entries, sale value) and a chat span for each model call, so one slow or wrong line can be traced to the step that caused it. [TODO: screenshot of one bill's trace]

The failure that matters, and the guard

The fourth bill is the Hindustan Unilever distributor's tax invoice: 18 lines, 14 columns, tiny print, photographed at an angle and compressed twice. The first version of BillSahi read it and reported that one Horlicks line was overcharged by ₹110.20, charged 18% instead of 5%.

That was wrong. Horlicks was billed at 5%. From line 15 down, the model had shifted every row by one: each item's name was paired with the next line's numbers, and the 18% belonged to the Vim bar below it. The last "line" it read was the bill's total row.

For a shop, a false overcharge is the worst mistake this app can make. The owner calls the supplier, the supplier shows him the paper bill, and he never trusts the app again.

So now BillSahi checks its own reading before it says anything about money. The lines must add up to the total printed on the bill. On this bill they added up to ₹27,060 against a printed ₹13,745. When they don't match:

  • no line is marked overcharged or undercharged; they go back to "check this line on the paper bill";
  • none of the bill's prices are saved, so a misread price can't later produce a false "you paid less before";
  • the receipt is stamped RETAKE PHOTO instead of RATES OK, with a note to send the original photo instead of a forwarded one.

On the other three bills the lines matched the printed totals to the rupee: ₹17,530, ₹3,648 and ₹15,252. The check runs in code after every read (ledger.reconcile in app/ledger.py).

Results on the item test set

Before the shop visit I wrote a test set of 57 shop items across kirana, personal care, hardware, stationery, medical, textile and footwear, labelled against the notification text and frozen before training.

Approach Heading right Rate right Wrong rate stated confidently Median time
Plain search (BM25, no model) 42% 68% 16% 2.8 s
Qwen3.5-4B, base 65% 28% (asks 68% of the time) 0% 5.6 s
Qwen3.5-4B fine-tuned on Tinker (v1, step 200) 67% 84% 16% 5.8 s
Gemma 4 31B (hosted) 91% 100% 0% 23.3 s

Fine-tuning moved the 4B model from 28% to 84% of rates right, at about a quarter of Gemma's time per item. But it still stated a wrong rate confidently on 16% of items, the same as plain search. Its failures were brand names (it filed several under 1511, palm oil) and the price thresholds. So the live app runs Gemma 4 31B, which got every rate right on this set, and the fine-tuned model stays switchable with one setting (HSN_MODEL_PATH). I'd rather ship the slower, correct model than the fast one that is confidently wrong one time in six.

The training run died at step 209 of 647 when my laptop ran out of memory, and my Tinker credit ran out with it. The v1 numbers above are from the step-200 checkpoint. That's honest, and it's where the next ₹49 subscriptions would go.

Why Does Open Innovation Matter?

Price. At ₹49 a month and a few bills a month, a check can't cost more than a few rupees, even with 30 lines on a bill. Open models are what make that possible: Gemma 4 runs on free and cheap hosted endpoints today, and a fine-tuned 4B model runs on hardware a small company can own. A per-token closed API with a 30-line bill and two calls per line would eat that ₹49 quickly.

The data stays the shop's. Supplier bills are a shop's purchase prices, margins and suppliers. With open weights the whole thing can run where the shopkeeper's data already lives, instead of every bill going to whoever owns the model.

The niche is the point. GST 2.0 moved toothpaste, hair oil and toilet soap to 5% and left skin creams at 18%. A general model has no reason to know that the aloe vera gel on this bill is in the 18% heading, or that "PCNO 500ML FT" is coconut oil. With open weights I can train exactly that niche, check it against the official text, and hand the arithmetic to code.

Where a closed model would do better, honestly: reading the blurry HUL bill. A larger closed vision model might have read it in one go. BillSahi's answer for now is to notice when it has misread a bill and say so.

My First User

The owner of a supermarket near Pavagada, Tumkur, gave me his last four supplier bills and answered my questions. What changed because of him:

  • I stopped treating the GST rate as the main problem. His suppliers charge the right rate: of the 31 lines on his three GST bills, I checked every one by hand, and the only wrong ones were the 2 aloe vera lines.
  • I added the credit check because he's moving from composition to regular GST in January. Every one of his bills would fail it today.
  • I added price memory because "a few suppliers charge more" was the first thing he said.
  • I added the supplier book because tracking credit is, in his words, a big problem.
  • ₹49 a month is what he said he would pay, for two or three uses a month.

He hasn't paid yet; BillSahi isn't a business yet. But it is now the tool he described, not the one I walked in with.

Prize Categories

  • Gemma: Gemma 4 31B reads every bill photo (streamed vision), places every line in the live app, and got 100% of rates right on the 57-item test set. Gemma 4 also wrote half of the training data and blind-checked the other model's items.
  • Tinker: Qwen3.5-4B LoRA fine-tuned on Tinker, from 28% to 84% of rates right (table above), plus Qwen3.5-397B on Tinker for training data. Switchable in the live app with HSN_MODEL_PATH.
  • Render: the web app and API run on Render: https://billsahi-pb7k.onrender.com, with background jobs so the page prints each line as it's checked.
  • Tiger Data: BM25 search with pg_textsearch over the 12,604-code HSN directory and the 1,570 official entries, and the shop's book (bills, lines, payments) in the same Postgres.
  • Sentry Agent Tracing: an agent span per item with tool and chat spans for every step. [TODO: screenshot]

What's next

  • The owner sends the next bill from the same HUL distributor, as an original photo, so price memory gets its first real repeat.
  • WhatsApp: forward a bill to a number and get the check back, since that's how the bills arrive anyway.
  • Finish the fine-tune (v2, with the brand-heavy retail data) so the cheap model can take over from Gemma.

BillSahi is a helper, not tax advice. Anything marked "check" or "ask" should be confirmed with an accountant.

AI Assistance Disclosure: I have used Anthropic's Claude Code for planning and helping me to draft this post.

來源:DEV Community · dev.to