This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle.
What I Built
InvoiceSort is a month-end receipt sorter for freelancers. It files a photo into a tax bucket, totals the month, and exports CSV — without uploading the file.
Situation. Month-end looks like this: a camera roll of Uber screenshots, a Figma invoice, coworking rent, and a supermarket run that is not a business meal. The accountant wants a spreadsheet. Hosted tools want the photo. When the filename is a client's, that is a deal-breaker.
Task. Get every receipt into a bucket an accountant recognises, show the month in one currency, and hand over a CSV — with the client's file never leaving this laptop.
Action. You drop a photo, paste the text, or click See a demo. Tesseract.js reads the image in this tab. A parser owns the merchant, date, total, and line items — the model is not allowed to invent a number. Shop-name sorting is instant. Then MobileBERT-MNLI, an open-weight NLI checkpoint, runs on-device through Transformers.js and names the tax category: Travel, Meals, Software, Hardware, Office Rent, Utilities, Marketing, Professional Fees, Taxes, or Other. You can override any label. Mixed $ € £ ₹ convert for the dashboard; CSV keeps the charged amount.
Result. Categories, month totals, a chart, and a CSV Excel will open. Whole Foods stays Other. There is no account and no server. After one ~25 MB model download, the next hundred receipts cost electricity. That is who would pay: a solo freelancer or tiny studio who will not send a client invoice to someone else's GPU, and who will not pay a per-page API bill for a job that happens once a month.
The product UI never names the model. A freelancer sees what it is for, why it exists, what they get, then See a demo. Badges read On this device, Auto, and You. MobileBERT-MNLI is the engine, not the copy.
I am not entering partner prize categories. The overall prize does not require them. This build does not run on Render or DigitalOcean, does not ship Gemma, and does not use GitHub Copilot. GitHub Pages is the live demo, not Copilot.
Demo
Live demo: https://surajns0033-collab.github.io/invoicesort/
No account. Click See a demo. Eight fake receipts should appear immediately — do not wait for a spinner. The on-device model can keep downloading in the background (~25 MB).
- Read the three cards: What it is for, Why it exists, What you get.
- Click See a demo.
- Confirm Whole Foods is Other, Uber is Travel, Figma is Software, Jio Fiber is Utilities.
- When the header chip flips to On this device, hit Re-sort categories if you want a second pass. Shop-name sorting already filled the list.
- Change a category by hand — the badge switches to You.
- Click Travel and confirm Total Expenses becomes only that category.
- Switch Show prices in from USD to INR — every total, line item, and chart follows.
- Export CSV. Open it in Excel.
Local copy, if you want the COOP/COEP headers that GitHub Pages cannot set:
node tools/serve.js
# http://localhost:5173
Code
surajns0033-collab
/
invoicesort
Local-first freelancer receipt sorter. Client files stay on the device.
InvoiceSort
A receipt sorter for freelancers. Photos stay on the laptop. The model names the tax bucket. The parser owns the money.
Drop a photo or paste a receipt. InvoiceSort reads it in this tab, files it into a category an accountant recognises, totals the month in one currency, and exports CSV. There is no account and no server.
Quick start
Needs Node.js 18+. No install step, no build.
Live demo: https://surajns0033-collab.github.io/invoicesort/
node tools/serve.js
# open http://localhost:5173
Click See a demo. Eight fake receipts sort from the shop name immediately; a stronger pass can run later in this tab. Serve over HTTP, not file://.
node tools/smoke-test.js # parser + currency conversion
node tools/check-dom.js # HTML ids match CSS
Why this exists
Month-end for a freelancer is a camera roll of Uber screenshots, a Figma invoice, coworking rent, and a supermarket run that is not a business meal. Existing…
MIT licensed. Tests:
node tools/smoke-test.js
node tools/check-dom.js
The open-weight model is Xenova/mobilebert-uncased-mnli (ONNX of typeform/mobilebert-uncased-mnli, Apache-2.0), loaded with Transformers.js 4.3.1. The model id lives in js/model.js. App code is MIT; third-party notices are in NOTICE.
| File | Role |
|---|---|
js/app.js |
OCR, parser, currency display, charts, CSV |
js/model.js |
Transformers.js + MobileBERT-MNLI |
js/samples.js |
Eight fake receipts so a judge never uploads a real invoice |
tools/serve.js |
Static server with COOP / COEP so the WASM runtime can load |
How I Built It
Hybrid on purpose. A classifier is a poor extractor, and a tiny generative model is a poor accountant. InvoiceSort does not ask the model for JSON or a total. The open-weight model is at the core of the category, which is the part a freelancer would pay someone to get right — and the part that must stay on the device.
flowchart LR
A[Photo or pasted text] --> B[Tesseract.js OCR]
B --> C[Deterministic parser]
C --> D[Amounts / dates / lines]
C --> E[MobileBERT-MNLI in-tab]
E --> F[Tax category]
D --> G[Dashboard + CSV]
F --> G
E -.load failed.-> H[Keyword fallback]
H --> G
-
Parser (
js/app.js) owns money. LastTotal/Amount dueline wins. Dates settle month-vs-day from the whole receipt. A date is never a line item.₹/€/£/$are detected, not assumed. The dashboard converts those into one display currency with a local rate table. CSV keeps the charged amount. -
Model (
js/model.js) callspipeline('zero-shot-classification', 'Xenova/mobilebert-uncased-mnli')with one hypothesis per freelancer tax bucket.Otheris an explicit hypothesis for groceries and clothes, not a leftover. NLI runs on every receipt. If the parser already has a confident merchant match — or has already called it personal spend — that category stays. NLI is the tie-breaker for unknown merchants, not a licence to turn Whole Foods into Hardware. - Fallback is weighted keyword scoring over merchant name and line items, so the app is usable while ~25 MB of ONNX downloads.
-
Packaging: static files, no build, no backend. Persistence is
localStorage. Clear wipes it.
I first tried Gemma 3 270M as a generative classifier. That was the wrong job: a 260 MB download, WebGPU/WASM dtype ladders, and a model that wants to write paragraphs when the product needs a label. MobileBERT-MNLI is the open-weight model that actually fits a side hustle someone would open on café wifi. Open weights made that swap possible. A closed API would have locked the product to whatever that vendor ships.
Why Does Open Innovation Matter?
This only works as a business if inference is free at the margin and the files never leave the device.
- Privacy I can print on the landing page. The receipt image is OCR'd in-tab. The NLI model sees a short text snippet in the same tab. Nothing is posted to an API. That is the product, not a footnote.
- Cost that survives the first user. A hosted vision model that charges per page will eat the freelance fee. MobileBERT downloads once (~25 MB), then the unit cost of the next hundred receipts is electricity.
- No lock-in. The model id lives in one file. A later Apache-2.0 ONNX build is a swap. The parser does not care.
- The right open model, not the largest one. Open weights made it possible to change the model when Gemma 270M was the wrong shape.
Closed would have been faster to demo and worse to sell.
The Week 2 prompt asked for something someone would actually pay for. A freelancer will not pay for a chatbot that might round the total. They will pay for a sorter that never uploads the receipt and that refuses to turn groceries into a deduction.
This article was created with the help of AI.




Top comments (0)