This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
TL;DR: Small Indian shops get orders on WhatsApp in a mix of Hindi, English and shorthand. ParchiMatch Lite turns each line into a checked billing row, on the shop's own 8 GB laptop with the Wi-Fi off. When it isn't sure, it asks one short question instead of guessing. On 80 locked test orders it filled in 0 wrong rows, and on messages my friends typed by hand, also 0.
What I Built
This is built for small Indian stores. Their orders come in on WhatsApp, and one message can mix Hindi, English and abbreviations:
Namaste bhaiya
kit kat 10/- darjan 4
red label adha kg 2 box
12 pack lais peela 10 wala
teen petti marrie 10 rs
Kal subah tak bhej dena
Someone has to retype every line into billing software like Tally, Busy or Marg. Petti is a carton, darjan a dozen, adha kg half a kilo, lais peela the yellow Lay's. And if a line is matched to the wrong product, that's expensive: the wrong stock gets packed, billed and put on the van.
So I tried to build something that needs minimal input from a person, but asks for their full attention exactly where a mistake would cost money. It reads each line, matches it to the shop's own catalogue, and sorts it into one of three colours:
| 🟩 Green | Matched and checked: teen petti marrie 10 rs → 3 cartons of Marie ₹10 (180 packets) |
| 🟧 Amber | Not sure, so it asks one short question with its best guess filled in: marie 30 wala 20 jodi → What is "jodi"? packet or carton?
|
| ⬜ Grey | Greetings and notes, ignored: Kal subah tak bhej dena
|
Then it exports a CSV for the billing software. It also learns the shop's own words: confirm "lais peela" once with Remember ticked, and next time it goes straight through.
The rule behind everything: a wrong row must never be filled in on its own. When unsure, ask. A shop owner forgives a question. They don't forgive a wrong delivery.
Who would pay: small FMCG wholesalers and kirana suppliers who get dozens of WhatsApp orders a day and pay someone to retype them. It runs on the laptop they already have, works without internet, and costs nothing per order.
Pricing (an untested guess): around ₹299 a month per shop. I haven't put this price in front of a real shop yet; that's the first thing I'll test.
Demo
The whole video is recorded with Wi-Fi off, on a MacBook Air M2 with 8 GB of RAM. Even the project site in the last minute is served from the laptop. What you'll see:
- A real-looking order, matched in about 8 seconds: 6 lines green, 5 amber, 2 notes.
- The model's guess can be wrong, and that's why it asks: for "lais peela" (the yellow Lay's) it suggests Magic Masala, but yellow is Classic Salted. One tap to pick the right one, and tick Remember.
- It learns: after export it says "Learned 3 new names", and the same order comes back with 8 green lines instead of 6, with the corrected products.
- Four tricky lines, and what the app does with each:
| I typed | What happened | Why |
|---|---|---|
aashirvaad atta 10 kg 1,500 bag |
🟩 1,500 bags | An early version read this as 1 bag and accepted it (see What I got wrong) |
maggi 1,5 pkt |
🟧 What does "1,5" mean? | An unclear number is never guessed |
red label adha kg 2 box |
🟧 Is it Brooke Bond Red Label (250 g)? | The model guessed 250 g; adha kg means 500 g. The code saw two sizes fit and asked |
पारले जी 5 वाला 2 पेटी |
🟩 2 cartons Parle-G ₹5 (288 packets) | Hindi in Devanagari works too |
Project site: https://bipul724.github.io/parchimatch/
Code
ParchiMatch Lite
Messy wholesale orders → clean billing rows, on the shop's own computer.
A small FMCG wholesaler gets orders like these on WhatsApp:
Namaste bhaiya
parle g 5/- 2 petti
tata namak 1 kg 5 ctn, lays hara 10 rs 3 box
पारले जी 5 वाला 2 पेटी
fortune oil 2
Kal subah tak bhej dena
Someone retypes every line into Tally, Busy or Marg. ParchiMatch Lite reads each line, matches it to the shop's own catalogue, and either fills in a checked row or asks one short question. It never guesses a number, and it never auto-accepts a row it can't justify. It runs fully offline with an open-weight model on an 8 GB laptop.
| 🟩 Green | Matched and checked: parle g 5/- 2 petti → 2 cartons Parle-G ₹5 (288 packets) |
| 🟧 Amber | One question, best guess pre-filled: marie 30 wala 20 jodi → What is
|
Zero npm dependencies; Node 24 runs the TypeScript directly. To try it: npm run app -- --open.
How I Built It
The model only picks the product. My code reads every number.
Small models are good at "which product did they mean?" and unreliable at "is petti a packet or a carton?". So the work is split:
-
Code reads every number: quantities, unit words (petti, ctn, darjan, bori…), prices (
5/-,10 rs), pack sizes (1 kg,500ml), Hindi number words (teen, paanch). - Fuzzy text matching handles most lines instantly, with no model at all.
- Only when fuzzy matching can't decide does the local model see the line plus the 8 closest products. It answers in strict JSON, at temperature 0, and can only pick one of those 8.
- 16 safety rules in code check the pick: does the price match? The pack size? Is the product sold in that unit? Any failed check turns the row amber, and the question is written by the code, not the model.
Choosing the model: I tested five
On my 8 GB MacBook I tried five open-weight models in that "decider" role, on 320 synthetic practice orders:
| Model | Auto-accepted | Wrong rows | No typing needed | Right suggestion on amber rows | Typical / slowest |
|---|---|---|---|---|---|
| Gemma 4 E2B (chosen) | 88.7% | 0 | 96.1% | 65.2% | 3.1 s / 9.2 s |
| Qwen3.5 2B | 88.7% | 0 | 94.6% | 52.2% | 2.5 s / 3.3 s |
| Gemma 3 4B | 88.7% | 0 | 95.6% | 60.9% | 5.3 s / 8.4 s |
| Qwen3.5 4B | 88.2% | 0 | 95.6% | 62.5% | 5.2 s / 8.4 s |
| Gemma 4 E4B | – | 0 | – | – | 5.6 s / 37 s |
I picked Gemma 4 E2B because when it isn't sure, its suggestion is right far more often, so the shopkeeper taps Confirm instead of typing.
Then I tested it once, on orders I had locked away
Before tuning anything, I set aside 80 test orders and locked them; CI fails if they change. Each method ran on them exactly once:
| Method | Handled with no review | Wrong rows auto-accepted | No typing needed |
|---|---|---|---|
| Exact match | 54.9% | 0 | 54.9% |
| Fuzzy match | 84.3% | 0 | 84.3% |
| Model on every line | 84.3% | 0 | 92.2% |
| Hybrid (the app) | 88.2% | 0 | 92.2% |
The model was needed on only 31 of the 80 lines, about 1.5 s each.
Watching the model be wrong, with Sentry
I traced an evaluation run in Sentry, one trace per order line, with steps for fuzzy matching, the model call and the safety rules. My favourite trace is the Red Label line.
Adha kg means half a kilo, so the 500 g pack. The model answered the 250 g pack, with status "ok", confident and wrong:
The safety rules didn't trust it. Both pack sizes fit the order's words equally well, so the line went to a human:
The model suggests; the code decides. The trace also shows every model call going to http://127.0.0.1:11434: the laptop itself, not a cloud. Across all 320 practice orders: 113 model calls, 640 tool calls, 58K tokens, $0.
Tracing lives only in the evaluation tool, runs only on synthetic data, and only when switched on. The shop app itself sends nothing anywhere.
CI keeps me honest
Every push runs 210 tests on GitHub Actions, checks that the locked test orders haven't changed by a single byte, and posts the no-model baselines to the run page.
What I Got Wrong
- My first design asked the model for everything: product, quantity and unit. During development it was the worst design I measured, and on the final test it only tied plain fuzzy matching. That's why the model now picks only the product.
-
"1,500 bags" became 1 bag, and went green. The comma split
aashirvaad atta 10 kg 1,500 baginto two lines. A wrong row, filled in on its own: the one thing the app must never do. Now Indian and Western digit groups (1,500,1,00,000) are read as one number, and anything unclear like1,5gets a question. - "Remember" could learn the wrong size. "tata tea premium" was amber only because the order didn't say which size. Learning that name would have auto-accepted one size forever. Now only names the catalogue doesn't know can be learned, and a learned name means the product, never one pack size.
-
The model won near-ties for a silly reason.
lays 10 wala 2 ctnnames no flavour, but the model's pick, Classic Salted, led by 0.074, only because its alias was short. Now a model pick is accepted only if the order's own words put it ahead by at least 0.08, the same margin fuzzy matching needs. That was the last wrong row in development. - I tried a bigger model. Gemma 4 E4B needs about 6 GB; on 8 GB, only 2.5 GB fit on the GPU and the slowest line took 37 seconds. On a shop laptop, RAM is the limit, not disk.
- I nearly trusted my friends' first replies. They were written by ChatGPT, not typed by hand. If I hadn't noticed that all four had the same shape, my "real-world" test would have been one more synthetic test (the full story is under My First User).
Why Does Open Innovation Matter?
- The orders are the business. Customer names, what they buy, at what price: a shop won't paste that into someone else's cloud. With an open-weight model, nothing leaves the laptop. The app only listens on 127.0.0.1.
- It costs ₹0 per order. No API bill growing with every WhatsApp message. That's what makes small-shop pricing possible.
- It works when the internet doesn't. Market internet is unreliable; this runs fully offline, as the video shows.
- I could test and choose. Because the weights are open and run locally, I could put five models through the same orders and pick the one that fits an 8 GB laptop.
My First User
Honestly, I don't have a paying user yet. No shopkeeper has used ParchiMatch. So I did the next best thing: I asked friends to type a restock order the way they'd really send it to a supplier.
The first replies were suspiciously perfect. Four messages, each exactly 17 lines, every item in the same order as my list, one a light rewording of another. My friends had asked ChatGPT. 😄 I threw them away and asked again, with six items and "please type it yourself."
Three real, messy messages came back, like this one:
bhai kal subh tk ye dal dena parle g 5wla 2 petti tata nmk 1 bori fotune tel pouch 1l 3 petti amul btar 100g 20 pis dairy milk 20rs 2 dabba or lifeboy 10 sabun 1 ptti bil sth bhej dena
I labelled the 11 order pieces, locked them, and scored them once:
- 0 wrong rows filled in, and 80% needed no typing.
-
One strict miss:
fortune oil 1 ltr ke 1 cartonwent through without a question, though I had labelled it as worth asking (pouch or bottle?). Its row matched my label exactly, but by my own strict rule it counts, so I report it. - The app asked instead of guessing on every hard piece. For items joined with aur, though, its suggestion names only one product, so a busy operator could miss the second.
It's 11 pieces from 3 people: a signal, not proof.
What It Can't Do (Yet)
- Items joined by aur, or sent with no separator, aren't split. The app asks about them, but its suggestion names only one product.
- "Pouch" isn't a unit word yet. And Tata namak fits Tata Tea about as well as Tata Salt, so it asks.
- Tested on synthetic shop data plus 11 real pieces, not on a real shop's catalogue yet.
- It exports a CSV; it doesn't write into Tally or Busy directly.
- Tested only on a MacBook Air M2 with 8 GB. Windows and Linux are untested.
The next step is one real shop and one week of real orders.
Prize Categories
-
Best Use of Gemma: Gemma 4 E2B (
gemma4:e2b-it-qatvia Ollama) is the model in the app, chosen by testing five models and confirmed once on locked test orders. -
Best Use of Sentry Agent Tracing: the evaluation tool sends
invoke_agent,chatandexecute_toolspans for every order line. The traces above show a confidently wrong model answer being caught by the safety rules. - Best Use of GitHub Copilot: entered through the GitHub Actions route. CI runs the tests, the locked-test guard and the baselines on every push.
If you run a shop, or know someone who does: clone it, load your own catalogue as a CSV, and paste in yesterday's WhatsApp orders. It never needs the internet, and it will ask before it guesses.





Top comments (0)