This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
What I Built
My flatmate Akshay and I split groceries constantly. One of us pays for the dal, the other for the gas cylinder, and by the end of the month neither of us remembers who owes what. We tried a shared spreadsheet twice and both times it was dead within two weeks — logging a purchase meant opening a file, finding the right row, and doing the arithmetic yourself, which is exactly the friction that makes people stop bothering.
So I built SplitBasket for him. You log a purchase once — item, amount, who paid, the date — tick which flatmates shared it, and that's the whole job. It categorises the item on its own, computes the equal split, and keeps a running balance of who owes whom.
What it actually does:
- Log a purchase in one small form: item, amount, payer, date.
- Tick who shared it — the equal split is worked out for you.
- Auto-categorise into staples, cooking, fresh produce, dairy & eggs, gas & utilities, snacks & drinks, household, personal care, or other.
- See the month at a glance — total spend, your share, your live balance, and a spend-by-category breakdown.
- Settle up — a plain snapshot of who owes whom, no interpretation needed.
- Export to CSV whenever you want the raw numbers.
The part that matters most to us: it all runs on the laptop. The entire ledger lives in a single SQLite file on the machine, and the app works with no internet connection at all.
Demo
The clip walks through logging a grocery run: I type the item, the category appears as I type, the split is computed across all three flatmates, and the new entry lands in the settle-up view.
I handed it over last week — he stopped asking me what he owes, which was the entire point.
Code
https://github.com/AmanDevelops/SplitBasket
How I Built It
The whole thing is deliberately small. It's a Node.js app (Node ≥ 22.5) with zero runtime dependencies, and the open-source AI is the core of how it thinks, not a decorative add-on.
Storage — node:sqlite. Node now ships SQLite in the standard library, so the database is the built-in driver: no ORM, no install step, no external service. Three tables — members, transactions, and transaction_shares — with foreign keys on and the split stored as rows, so a purchase and its shares are always consistent.
Categorisation — local rules first. A grocery-aware rule set maps keywords to categories. It's tuned for how people in an Indian flat actually write their shopping lists, so it recognises Hinglish alongside English: dal, atta, chawal, sabzi, pyaz, doodh, paneer, tel, haldi, namak, chai, gas cylinder. Type "Toor dal 1kg" and it lands in Staples without asking anyone.
Categorisation — an open-weight model when there's one to use. If Ollama is running locally, the server first asks Gemma 3 (1B) — Google's open-weight model, pulled with ollama pull gemma3:1b — to classify the item. The request asks for a strict JSON shape (category, confidence, reason), runs with a 3.5-second timeout, and is parsed defensively. If the model is missing, slow, or returns anything unusable, the code falls back to the local rules automatically and the app keeps working exactly as before.
npm start # then open http://localhost:3000
ollama pull gemma3:1b # optional: richer categorisation
OLLAMA_MODEL=gemma3:1b npm start
The front end is plain HTML, CSS, and JavaScript served by the same Node process — no build step, no framework, nothing to install.
Why Does Open Innovation Matter?
This is the part where open wasn't a preference — it was the right engineering call for this particular project.
The data never leaves the device. A grocery ledger looks harmless, but it's a detailed record of what three people eat, when they cook, and what they spend. With local inference, my flatmate's habits stay on our laptop instead of sitting on a server neither of us controls. For something this personal and this small, "we promise we won't look" isn't a good enough answer.
The behaviour is inspectable and changeable. The categoriser is plain rules in a plain file. When my flatmate told me "that's personal care, not household," I changed one line and it was fixed — no retraining, no prompt-tuning against an opaque model, no waiting on a vendor to improve a black box. With a closed API I'd have had a category I couldn't argue with.
The model is swappable, and its absence is survivable. It runs Gemma 3 (1B) today. If we want a larger model, a different one, or a fine-tune, we change a model name — the product doesn't care, because the model sits behind one function with a working fallback. That fallback is the real win: the app is fully functional with the model switched off entirely. A closed API can't offer that. If it's down, rate-limited, or repriced, your app is down with it.
It costs nothing to run. A flat's grocery ledger should not have a per-request cost. There's no key to rotate, no quota to watch, no bill that grows as the flatmate count does. The only running cost is the laptop we already had.
It works offline. No internet, no problem — which matters more than it sounds, because the moment the ledger stops working is the moment it stops being used.
None of this would have been possible with a closed hosted API without giving something up: the privacy, the inspectability, the offline behaviour, or the cost. Open-weight models and local inference let me hand a friend a tool that is private, honest about how it works, and genuinely his.
Prize Categories
Best Use of Gemma — SplitBasket uses Google's open-weight Gemma 3 (1B) via Ollama as its primary categoriser, running locally on the machine, with a rule-based fallback so the app stays fully functional when no model is available.

Top comments (0)