# Order Ledger: a WhatsApp-to-ledger side hustle that runs offline on open models

> Source: <https://dev.to/khushighosh/order-ledger-a-whatsapp-to-ledger-side-hustle-that-runs-offline-on-open-models-3nk0>
> Published: 2026-10-15 12:45:33+00:00

*This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle](https://dev.to/challenges/hacktoberfest-week2-2026-10-12)*

Small sellers run on WhatsApp. A home baker, a tiffin service or a tailor gets orders like this:

```
C: bhaiya kal subah 20 samosa aur 1 dozen kachori chahiye
S: ho jayega, 20 samosa 10 rs each = 200, kachori 12 x 15 = 180. total 380
C: thik hai, paise delivery pe dungi. Neha, 9000000002
```

Then they copy it into a notebook, and a week later nobody remembers who paid the advance.

**Order Ledger** turns that chat into a structured order, shortened here:

```
{
  "customer": "Neha", "phone": "9000000002",
  "items": [
    { "name": "samosa", "qty": 20, "unit": "pcs" },
    { "name": "kachori", "qty": 12, "unit": "pcs" }
  ],
  "delivery_date": "2026-10-14", "delivery_time": "morning",
  "amount": 380, "paid": 0, "status": "confirmed"
}
```

The seller checks it on a review screen, saves it, and gets:

**Who would pay?** Anyone who takes orders in chat and tracks them by hand: home bakers, cloud kitchens, tailors, small shops. The pain is the retyping and the money that slips through. Customer names, numbers and prices are also private, which is why the whole thing runs on the seller's own laptop.

It handles English, Hinglish and Kannada in English letters, and it accepts exported WhatsApp chats as they are.

The demo shows: pasting a WhatsApp-style chat, reading the order, fixing a field on the review screen, the schedule and pending-payments tabs, and then **Wi-Fi switched off** with one more order processed.

Turn messy WhatsApp order chats into a clean order book. It runs entirely on your own computer with an open-weight model, so customer names, phone numbers and prices never leave your laptop.

Built for the Hacktoberfest Open-Source AI Challenge, Week 2 (Side Hustle).

Home bakers, tiffin and cloud-kitchen sellers, tailors and small shops who take orders over WhatsApp and keep track in a notebook. Their chats are messy: English mixed with Hindi or Kannada, "kal subah", "advance 500 diya", orders that change halfway through. You paste the chat, and the app turns it into a structured order.

`C:` for the customer and `S:` for you) and extracts the customer, phone, items, quantities, delivery date and time, total, amount paid and status.
MIT licensed. There is nothing to `npm install`:

```
ollama pull qwen2.5:7b
git clone https://github.com/Khushighosh/Order-Ledger.git
cd Order-Ledger
node server.mjs      # then open http://localhost:3000
```

**The open pieces**

`qwen2.5:7b` (4.7 GB) as the default and `llama3.2:3b` for comparison`http` and `fetch`), one HTML file for the UI, and a JSON file for storage

```
pasted chat ──► convertChat()  WhatsApp export → "C:" / "S:" lines (in the browser)
            ──► annotateDates() "kal" → "kal (2026-10-14)"
            ──► Ollama (qwen2.5:7b, temperature 0, JSON-schema output)
            ──► clean()         validate phone, units, dates (server)
            ──► review screen ──► orders.json ──► schedule / pending / CSV
```

Ollama accepts a JSON schema as `format`, so the model's reply always parses and uses only allowed values:

```
unit:   { anyOf: [{ type: "string", enum: ["kg", "pcs", "litre", "box"] }, { type: "null" }] },
status: { type: "string", enum: ["confirmed", "tentative", "cancelled"] },
```

The schema guarantees the **shape** of the answer, not that it's **right**. That gap is why there's a review screen and a test harness.

Small models are unreliable at date arithmetic, so the date lookup happens in code and the model only reads the result:

``` js
export function annotateDates(text, cal) {
  const rules = [
    [/\bday after tomorrow\b|\bparso\b/gi, cal[2].iso],
    [/\b(?<!after )tomorrow\b|\bkal\b|\bnale\b/gi, cal[1].iso],
    [/\baaj\b|\btoday\b/gi, cal[0].iso],
  ];
  let out = text;
  for (const [re, iso] of rules) out = out.replace(re, (m) => `${m} (${iso})`);
  return out;
}
// "nale 2 litre dosa hittu"  →  "nale (2026-10-14) 2 litre dosa hittu"
```

Phone numbers are cleaned in code too, because the model kept returning `+91 90000 00009`.

I wrote 18 synthetic WhatsApp-style chats (fake names and numbers) and hand-labelled the expected JSON for each. `evaluate.mjs` scores 9 fields per chat and breaks results down per field and per kind of chat (Hinglish, Kannada, same-day orders, "same as last time", and so on). With `temperature: 0`, re-running an unchanged prompt gave identical results, so every prompt change could be A/B tested.

| Test set | Model | Before the date fix | After the date fix | 
|---|---|---|---|
| 12 chats (I tuned on these) | qwen2.5:7b | 11/12 orders correct · 99% of fields | 9/12 · 96% | 
| 12 chats | llama3.2:3b | 1/12 · 82% | 2/12 · 84% | 
| 6 fresh chats | qwen2.5:7b | 3/6 · 91% | 5/6 · 96% | 
| 6 fresh chats | llama3.2:3b | 3/6 · 89% | 3/6 · 91% | 

On a laptop the 7B model takes about 14 seconds per order once loaded (about 50 seconds for the first order while the model loads). The 3B takes about 7.

`2026-10-20`, and for "nale" (Kannada for tomorrow) it answered `2026-10-17`. The cause was that the date table in my prompt listed "Tuesday: 2026-10-20" (next week), and the test day was a Tuesday. The model read "today is Tuesday" and looked it up. I fixed it by labelling that row "next Tuesday" and moving the relative-date lookup into code. The fresh set went from 3/6 to 5/6.
**An honest note on the numbers:** I used both test sets to tune the prompt, and the date fix came after looking at the fresh set's failures. Treat these as indicative, not a benchmark. That's the reason the review screen exists.

`127.0.0.1`, and orders live in a local file.` MODEL=...`). With a closed API I couldn't have inspected the setup this way, and an API-side model update could silently change the results.`temperature: 0` gave identical results on re-runs, which made the debugging above possible.
**The trade-off, honestly:** I didn't benchmark a closed API, so I'm not claiming open models are more accurate. A hosted frontier model is probably better at the tricky chats and faster per order. What open models gave me is privacy, a zero running cost and full control. For a seller who handles customers' private data on a laptop, that was the better deal.

I built this in a long chat session with Claude as a pair programmer. It drafted the first versions of the prompt, server and UI, and I ran everything on my own laptop, pasted the failures back, and made the decisions: which idea to build, which model to keep, what to revert. The best moments came from the failures, like the `2026-10-20` date bug.
