DEV Community

Cover image for PaperKeep
lalit sonawane
lalit sonawane

Posted on

PaperKeep

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

What I Built

Paperkeep is a receipt desk for freelancers and the bookkeepers who close their months.

You drop a PDF or a photo of an invoice. A small open-weight model, running on the laptop, pulls out the vendor, the date, the invoice number, the tax, and the total. You tag it with a client, fix anything that looks wrong, and export a CSV. Meals default to half deductible. A coworking pass lands in Office. A software subscription lands in Software. Those rules are a chart of accounts you can edit, not a generic "business expense" bucket.

The person who would pay is a freelance bookkeeper in filing week. They already have the tedious job: a shoebox of client invoices, a spreadsheet, and a hard rule that those invoices do not go to a chatbot. Forty receipts for one client, then another client after lunch. A tool that reads the page locally and hands back a CSV is worth an evening of their rate. A freelancer with one messy month would pay less, once, for the same reason: tax time is a weekend they would rather not spend retyping totals.

The sample set includes a GST tax invoice on purpose. It has a taxable value, a CGST line, an SGST line, and an invoice total, and every one of those looks like money. The booked total has to be 11,210, and the tax has to be the two GST lines added together, 1,710. That is the actual problem. Not "find a number."

Demo

There is no public URL, and that is the product. Paperkeep binds to side-hustle-sigma.vercel.app. Receipt text is scored in that process. The ledger is a JSON file on the machine.

git clone https://github.com/sonawanelalit574/side-hustle.git
cd side-hustle
npm install
npm run setup
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:5173 and choose File the sample month. You should get four receipts: a software subscription in USD, a coworking invoice in INR with two GSTINs, a flight, and a cafe receipt booked at 50%. Read a sample PDF runs the same GST invoice through the PDF text extractor before the model sees it.

npm run setup downloads the int8 weights once, about 180 MB. After that, scoring a receipt does not need the network. npm run check asserts vendor, date, tax, total, and category on the four samples.

Code

Paperkeep

A receipt desk for freelancers and the bookkeepers who close their months. Drop a PDF or a photo of an invoice. An open-weight model reads the vendor, date, tax, and total on the laptop. You get a ledger, a client tag, and a CSV.

Nothing in that path is a hosted model API. The extractor is GLiNER small v2.1 (Apache-2.0), running as int8 ONNX through @lmoe/gliner-onnx. Photos go through Tesseract. PDFs are read locally. The API binds to 127.0.0.1. The ledger is data/ledger.json on this machine.

Run it

npm install
npm run setup
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://127.0.0.1:5173

setup downloads the int8 weights once (about 180 MB) plus the tokenizer. The default cache is D:\paperkeep-models when that drive exists, otherwise .models in this folder. Override with PAPERKEEP_MODEL_DIR.

File the sample month, or drop public/samples/jaipur-yard.pdf. That one is a GST tax invoice: CGST and SGST should…

https://github.com/sonawanelalit574/side-hustle

How I Built It

The open model is GLiNER small v2.1 (Apache-2.0). It is a zero-shot named-entity model: you hand it a list of labels, and it marks spans. I run the int8 ONNX export from onnx-community/gliner_small-v2.1 through @lmoe/gliner-onnx, on CPU, with ONNX Runtime. No GPU is required.

The labels live in server/labels.ts:

  • company name
  • invoice date
  • invoice number
  • subtotal amount
  • tax amount
  • total amount
  • currency
  • payment method
  • gstin

Changing that list retargets extraction. Swapping the ONNX file (PAPERKEEP_ONNX_FILE, or a different folder via PAPERKEEP_MODEL_DIR) swaps the checkpoint. A larger GLiNER model can drop in without a new vendor.

Around the model, the desk is deliberately boring. PDFs are read locally with unpdf (PDF.js). Photos go through Tesseract.js. The UI is a small React app. The API is Express on localhost. Filed receipts sit in data/ledger.json.

GLiNER proposes spans. A second pass checks the arithmetic: subtotal plus tax should equal the total. If the model's total and the line that literally says "Invoice total" disagree, the line that makes the math work wins, and the receipt is marked so you can see which one was booked. On the GST sample, CGST and SGST are both tax spans, so they are summed instead of treated as two competing totals.

Categories are not the model's job. Hints in the chart of accounts map "cowork" to Office and "latte" to Meals. You can edit those hints in the desk. They are stored next to the ledger.

Why Does Open Innovation Matter?

A closed document API can read an invoice. It fails this job in three places that decide whether anyone would pay.

The document cannot leave. A bookkeeper's invoices have client names, amounts, and GSTINs. "We don't train on your data" is still a copy of the file on someone else's server. Paperkeep scores the text in-process. After the weights are on disk, that step does not call out.

The second hundred receipts have to be free. Filing week is not a demo of five uploads. Per-page pricing can move, and a side hustle priced like a bookkeeper's tool cannot absorb a vendor's new rate. Local weights cost the same on receipt one and receipt two hundred. The business works because inference is not a line item.

The niche is a list of labels, not a contract. A general invoice prompt is bad at one person's chart of accounts. This coworking space is Office. Meals are half. This client's GSTINs matter. In Paperkeep that is server/labels.ts and data/categories.json. GLiNER was built to take labels it was not trained on. I did not have to ask a platform to support GST invoices, and I can point the same desk at a different checkpoint when a bigger open model is worth the disk.

The part that is actually hard is which number is the total. Subtotal, tax, and amount due all look like money. An open extractor lets me keep the spans, the scores, and the arithmetic check on the machine, and show all three next to the source text. A chat API can do the same reading only if I ship it the document.

Top comments (0)