DEV Community

Faisal Ahmad
Faisal Ahmad

Posted on

CloseTab counts a stall's night note without inventing a rupee

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

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

A chai stall already writes the night on a scrap of paper. CloseTab turns that scrap into the bill they lock up with: expected sales, what is in the box, every cost, the gap, and what to buy tomorrow.

CloseTab. The hero says count the box before you lock it, and the Night kettle sample shows a gap of 490 rupees.

The person who would pay for this is not looking for another chat. They are locking a stall, a weekend table, or a small cafe. The note already exists. The missing piece is a close that does not invent a rupee, and does not send the cash total to a paid API.

This week a lot of side hustles are invoice sorters, and that is a real chore. Alberto Landa's local invoice reader is the one I kept next to this build, because the part that mattered was teaching the model not to guess. Dekita measured a local invoice sorter and found the money is a wash. Wellington Filipe's counter reader uses arithmetic to catch the lines a model gets wrong.

CloseTab is the same distrust of a guessed number, pointed at a different hour. Not the supplier invoice in the morning. The cash box at night.

What the night looks like

A daylight stall counter, a blank slip, and a steel cash tray.

The page opens on Night kettle:

stall: Night kettle
date: 2026-10-15
14 chai 20
9 samosa 30
2 bun-maska 25
samosa leftover 0
chai leftover 6
cash 680
upi 410
bought milk 90
bought gas 40
Enter fullscreen mode Exit fullscreen mode

Expected sales are 14×20 + 9×30 + 2×25 = ₹600. Counted money is 680 + 410 = ₹1,090. The gap is ₹490. Milk and gas are ₹130. Left after costs is ₹470. Samosa is sold out. Chai still has 6, which is above the restock line of 5, so it is not flagged.

Night kettle, line by line. Expected 600, counted 1,090, costs 130, gap 490, and 470 left after costs.

People do not write notes in one grammar. This counts too:

stall : named 23
23/10/2026
test - 230
test2 - 230
Enter fullscreen mode Exit fullscreen mode

Stall name "named 23". Date 23 October 2026. Two sales at ₹230. Expected ₹460. Nothing was written for cash, UPI, or card, so the box is ₹0 and the gap is −₹460. The page does not pretend those two lines were never sold.

The desk

The note stays on the left. The bill prints on the right. It is not a message thread.

Tonight's close. The Night kettle note is on the left and the printed bill is on the right.

In that shot the counter is ready and Gemma is not running, so the parser built the sheet. The rupees match the note either way. If Ollama is up and Gemma stays inside the numbers that were written, the page says Gemma read the note and the code counted the rupees.

Coins and a blank slip, which is the voice section's picture.

You can say a line and it is appended to the note. Hear the close reads the gap and each cost back. The bars move only while a voice is on.

How a number is allowed to exist

flowchart TD
  note[The night note]
  gemma[Gemma 2 2B on this laptop]
  parser[The parser]
  check{Every number was written in the note?}
  ledger[Python ledger]
  bill[The bill]
  note --> gemma
  note --> parser
  gemma --> check
  check -->|yes| ledger
  check -->|no, or Gemma did not answer| parser
  parser --> ledger
  ledger --> bill

Gemma, through Ollama, may read the words into a small JSON note: stall, date, lines, cash, UPI, card, and costs. The call gives up after eight seconds.

Python does every rupee after that. A price, a leftover, a quantity other than the implied 1, or a cost has to appear as a digit in the note. If Gemma uses a number that was never written, that reading is thrown out and the parser is used. A made-up 999 does not become a sale. Zero in the cash box is allowed when the note never mentions cash, because "nothing written" is not the same as an invented total.

A line with no price does not get filled in as ₹0. Expected sales, the gap, and profit stay blank until every sale has a price. A blank note returns no totals at all.

That split is the product. The model is there to read a messy line. It is a bad accountant. The ledger is the accountant.

The same grammar lives in closetab/parse.py and in web/src/ledger.ts, so the page can count before the API answers. Ten tests cover the ledger and the API, including the slash date and the dash prices above. GitHub Actions runs them on every push.

Run it

There is no hosted demo. I am not entering a hosting category. render.yaml is a blueprint, and it has not been deployed.

pip install -r requirements.txt pytest
pytest
uvicorn closetab.app:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

In another terminal:

cd web
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:5173. Optional, for the Gemma path: ollama pull gemma2:2b.

GitHub logo fab-c14 / closetab

End-of-day closer for market stalls. Gemma reads the note. Python counts the rupees.

CloseTab

End-of-day closer for market stalls and small cafes. Paste the night's note. Gemma reads it into a small JSON note. Python does every rupee.

Built for the Hacktoberfest 2026 Week 2 Side Hustle challenge. This repository started during that window.

The hero wave is the Waves component from React Bits, used from its published source. The ledger, the note parser, and the rest of the page are original.

Run it

pip install -r requirements.txt pytest
pytest
ollama pull gemma2:2b
uvicorn closetab.app:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

In another terminal:

cd web
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:5173. If Ollama is not running, the sheet still builds from the written lines and the page says so.

Tracks this project can enter

Only list a track in the DEV post after that piece is actually in use.

  • Gemma: local Ollama model gemma2:2b reads the note.
  • Render: render.yaml hosts the…

The hero wave is the Waves component from React Bits, used from its published source. The ledger, the parser, and the rest of the page are original. The mark in the corner is the CloseTab logo, a green tile with a torn receipt.

Why open innovation matters here

A closed chat API can read a note. It can also charge for every close, keep the night's takings on someone else's server, and fill a blank with a confident guess.

CloseTab is the other shape. The model runs on the laptop, so the cash note does not have to leave it. There is no token invoice for arithmetic a stall does every night. The model name is CLOSETAB_MODEL, so a later Gemma can replace gemma2:2b without a new ledger. And when the model is down, or when it uses a number that was never written, the close still happens from the text on the page.

Open weight is what makes the price of this product possible. The thing someone would pay for is the close. The model is there to read. The code is there so the rupees stay honest.

Prize categories

  • Best Use of Gemma. gemma2:2b through Ollama reads the note locally. The ledger rejects any number Gemma did not find in the text and falls back to the parser.
  • Best Use of GitHub Copilot. The qualifying piece is GitHub Actions. .github/workflows/test.yml runs the ledger and API tests on every push. Copilot did not write this project.

Top comments (0)