Engineering docs
Vending runbook (Rewards V2, 2026-11-01)
Last updated Nov 1, 2026 · 14 sections
For the ops person and the technician who looks after the Freebee vending machines and the
Rewards settings. Plain English; no code. Everything named here is in the ops portal at /ops
unless it says otherwise. The box that sits beside a machine is the vend agent; its own
install guide (hardware list, wiring, pairing command, systemd, troubleshooting) is
vend-agent/README.md. The API behind all of this is docs/API_CONTRACT.md §v2.0.
1. How a reward reaches a shopper
- A shopper plays the cricket game on a kiosk or a walk-up cabinet. Every innings ends with a result card: a QR and a 6-letter code.
- On their own phone (
/w/c/CODE, or the Freebee app) they verify their number once and claim. A reward becomes a coupon with a product (a cold drink, chips…) and a channel: counter (show the coupon code to a person), vending (type a PIN on a machine) or any. - A vending coupon shows a 6-digit vend PIN that lives 15 minutes. The shopper walks to a Freebee machine in that mall and types it on the keypad.
- The agent asks our server; the server locks the coupon for 90 seconds and tells the agent which slot(s) to open and for how much credit. The machine lets the shopper pick; the agent reports vended or failed.
- Vended: the coupon is used, the slot's stock goes down by one. Failed: the coupon is released, the same PIN still works, the shopper can try again or ask for a new PIN.
Points work the same way one step later: an innings that hits no rung earns points; past the threshold the wallet offers the points catalogue; a redeemed product is a coupon like any other.
2. Adding a machine
Vending → Add machine.
- Venue: the mall it stands in. PINs are bound to the mall, so a wrong venue means every PIN is refused with "this code is for a machine in another mall".
- Label: what shoppers and alerts call it ("Atrium snacks").
- Driver:
manual(no machine yet, a person hands the product over — see §9),mdb(our box with the MDB adapter plugged into the machine) orvendor_api(a vendor's own cloud; paste their credentials in the config box, they are stored encrypted and never shown again). - Where it stands: one line, up to 120 characters, shown on the shopper's phone next to the machine's name ("Ground floor, by the food court") and on the keypad's "try <machine>" line. Write it for a visitor who has never been in the mall. Empty = the mall's name is shown.
- Notes: for ops only (price units, who refills it, the adapter model). Shoppers never see them.
A new machine is Not paired until the agent takes a pairing code.
3. Pairing the agent
- Open the machine's page and press Pairing code. You get a short code valid 10 minutes; the page shows the exact command for the box.
- On the box, the technician runs that command (
freebee-vend pair CODE --api …, details invend-agent/README.md). The page flips to Paired within a few seconds and shows the agent version and whether its driver answered (driver ok). - A code works once. If it expires, press the button again; the old code is replaced.
The machine token lives only on the box. If the box is lost or replaced, Revoke the machine (§8) and pair the new box with a fresh code.
4. Slots, stock and refill mode
The slot map says what sits where. Slot codes are the machine's own selection codes (A1, B2… or whatever the machine prints on its buttons); the product is from the catalogue (§10); capacity and stock are counts; price is what the machine charges for that selection (the agent grants exactly that much credit, so the shopper cannot pick something dearer).
- Save writes the whole map. A slot you remove from the list is gone; a slot with stock above 0 is no longer "sold out".
- Refill mode is the same page with big buttons for the person who stocks the machine on a phone: minus, plus, Full (= capacity), one Save stock at the bottom. Do it while standing at the machine after the physical refill, not before.
- Stock goes down by one on every confirmed vend. The machine can also tell us a slot is empty (jam or sold out): the slot shows sold out and stock drops to 0 at once. Refilling (stock above 0, Save) clears it.
- The list card shows the stock summary "
stock/capacityacrosskslots ·jlow" and a band: red under 10 %, amber under 30 %. "N empty" counts slots at 0 or sold out. - The low line (Rewards V2.1): a slot is LOW when its stock is at or under
max(minimum, capacity × percent)— the house settings "Low stock percent" (20) and "Low stock minimum" (3) on/ops/rewards→ Vending — and EMPTY at 0 or when the machine says sold out. Refill mode shows the line per slot ("low at 3"), LOW / EMPTY badges, and lets you give one slot its own line (a slow seller can be "low at 1"). Each slot also shows the product's unit cost ("₹10 each · edit"): that is what the house pays for one, used by every spend figure (§14); edit it there or on the Products tab — it is one price per product, not per slot. - When a slot crosses the line (a vend takes it there, the machine reports sold out, you type a number at or below the line in refill mode, or the 5-minute sweep finds it) every ops user gets a slot low / slot empty message once, with a link to that slot — and the venue's refillers get their message (§13). It does not nag: the slot is told once until someone refills it above the line, after which the next crossing is told again.
- If a slot that a live campaign depends on hits 0, every ops user also gets the slot empty message (at most one per slot every 6 hours).
5. What the shopper sees
On the phone: the PIN in large digits with a countdown, the machines in that mall with their
location line, and "Keep this page open until your snack drops". A machine that has not
checked in for 15 minutes is marked offline only when another machine in the mall is online
(a counter-only manual machine never checks in and must not read as broken). With no machine in
the mall the page says "show the code at the counter". If the machine is down the coupon's
counter code still works: tell the counter to accept it.
On the keypad display (two lines): "Type your code" → "Checking…" → "Pick your snack" →
"Enjoy!", or one of: "Code not valid", "Code already used / in use", "Code expired, get a new
one", "Out of stock here, try <other machine>", "Machine busy, wait 60 seconds" (5 wrong PINs in
2 minutes), "Did not drop, try code again" (the same PIN still works), "Time ran out, try code
again" (nobody picked within 60 s), "Machine problem, ask at counter" (the machine accepted a
selection but never said whether it dropped — see §6), "Machine offline, use the counter", "Needs
re-pairing, call Freebee ops". The full table is in vend-agent/README.md.
At the counter: while a coupon is being vended at a machine, or is disputed, the portal's Redeem refuses the counter code with a sentence saying so, so a prize is never handed over twice.
6. Disputes and how to resolve them
A dispute is a vend the server cannot settle on its own. Three ways one appears:
- The agent took a PIN, the coupon was locked, and no vended/failed report arrived within 90 seconds (box crashed, network dropped, power cut mid-vend, or the machine accepted the selection and never reported a drop — the box shows "Machine problem, ask at counter" and deliberately reports nothing). The next time anyone touches that coupon (the shopper asks for a PIN, a machine tries the PIN) it is disputed on the spot; the background job every 5 minutes catches the rest.
- The machine reported failed for the second time on the same coupon: rather than let a coupon bounce for ever, that attempt becomes a dispute for a person to look at.
- A machine vended a coupon that the counter had already redeemed meanwhile: nothing is paid twice on paper, but you are told so you can check whether a product left the machine.
In every case the coupon is locked so it cannot be used elsewhere, and every ops user gets a vend needs checking message (time in IST) with a link to the request.
Vending → Requests → the "need your decision" cards (also reachable from the Today tiles). For each one:
- Read the machine's own vend counter or ask the person on site: did a product drop at that time? The agent's late verdict, if it reconnected, is shown on the row ("agent reported vended after the dispute window").
- Vended: the coupon is marked used, the slot loses one. Failed: the coupon goes back to the shopper, who can get a new PIN. Add a one-line note. Once resolved a row never changes.
- When in doubt choose Failed: the worst case is a second snack; the alternative is a shopper who paid with their time and got nothing.
The request ledger keeps every attempt forever (never deleted); the Requests tab lists the last 7 days by default with the coupon code, product, machine, slot, driver and result. It never shows who the shopper is.
7. Offline machine, out of stock
- Offline (no heartbeat for 15 minutes): you get one machine offline message per
episode. The shopper's keypad says "machine offline, use the counter"; their PIN stays valid.
Check power to the box and the machine, then network, then
journalctl -u freebee-vendon the box (README troubleshooting). A redeem needs the server (a lock is taken per vend), so the agent never queues vends offline by design. - Out of stock: the keypad says "out of stock here" and names the nearest Freebee machine in the mall that has the product; the coupon is untouched. If no machine has it, the shopper is told to use the counter. Refill, or change the rung's product (§10) if the product is gone for good.
- Jam / nothing dropped: the agent reports failed, the coupon is released (same PIN still works), the slot is marked sold out. Clear the jam, refill, Save. A second failure on the same coupon becomes a dispute (§6).
8. Switching the driver, revoking
- Switching driver (for example
manual→mdbthe day the adapter arrives): edit the machine, change the driver, Save. The box picks it up on its next heartbeat, no reinstall. The select is disabled while the machine has an open or disputed request: resolve those first. Moving away fromvendor_apialso removes the stored credentials. - Revoke: the box's token stops working at once (every call gets 401, its display says "needs re-pairing"). Use it when a box is lost, replaced or a mall contract ends. The machine record, its slots and its history stay; a new pairing code brings it back.
- Moving a machine to another mall: change its venue. PINs are bound to the mall, so a sticker left in the old mall fails cleanly.
9. The manual driver (a venue without a machine)
A venue can run the whole rewards flow before any machine exists. Create the machine record with driver manual and a label like "Counter, ground floor". Two ways to use it:
- With a box and keypad at the counter: the shopper types the PIN; the box prints "HAND
OVER: Chips 50 g" for the counter person, who presses Enter when handed over (or
nif they could not). The server sees a normal vend, stock and reports included. - Without any box: leave the machine unpaired and set the house setting
fulfilment.defaultto counter (§10). Coupons then carry the counter code only, and the counter person marks them used with Redeem in the portal as today. Flip the default tovendingwhen a machine is live.
Either way, a manual machine never heartbeats, so it is never reported offline.
10. The Rewards settings page (/ops/rewards)
Every value here changes the live game without a deploy. Each field shows its current value, the seeded default, who last saved it and when; Check figures runs a 20,000-innings simulation of the draft (hit rate per rung, any-reward rate, average points, liability per 100 players) before you save. Save validates everything together and keeps a history (the History tab). If someone else saved first you get "reload and apply your change again".
The snapshot rule. An innings copies the ladder, the points rates and the channel when it starts. A save takes effect from the next innings on that screen, never halfway through one, and nothing already minted or issued is rewritten. So the page is safe to edit during mall hours; the only thing to think about is what the next innings will promise.
Every save must carry the version you loaded (the page does this for you); a save that lands after someone else's is refused with "reload and apply your change again".
| Setting | What it does | During mall hours |
|---|---|---|
| Ladder rungs (name, rule, product, rank, daily budget, max value) | What the five cards on the screen say and pay. Rank 1 is the best; the highest rung hit wins. Daily budget (seeded 10 / 20 / 40 / 60 / 80 a day, best rung first) is how many of that rung the house pays per day before it falls through to the next; max value refuses a product dearer than that. | Safe. Check figures first (it shows the per-day liability with the budgets applied); a product must be active in the catalogue. |
| Dice odds | House odds for creatives that set none; must add to 100, out above 0. | Safe but changes the whole game's feel; prefer between campaigns. |
| Points per innings / per run | What a no-reward innings earns (seeded 5 + 1 per run). | Safe; every ledger row records the rate it was minted with. |
| Winners also earn points | Reward winners get the innings' points too. | Safe, from the next innings. Raises liability. |
| Redeem threshold | Points needed before the wallet offers the catalogue. | Safe. Lowering makes more wallets eligible at once; raising never takes a coupon back. |
| Points expiry days / reminder days | FIFO expiry and the WhatsApp reminder a week before (nightly 02:30 IST). | Safe; reminder must be less than expiry. |
| Max innings per phone per day | Innings that earn points per phone per day (UTC). Over it, a claim banks 0 points. | Safe. |
| Default fulfilment | counter, vending or any for coupons that do not say. | Set to counter until a machine exists in the mall; vending once it does. |
| Vend PIN life / wrong PINs per machine | PIN minutes (15) and the keypad lock (5 wrong in 2 min → 60 s). | Safe. |
| App bonus (mode, points, install points, per phone per day, banner text) | What claiming in the Freebee app earns, and the banner on the web claim page. Seeded off. Turn it on only when the app is in the stores and the engineer confirms WALLET_APP_KEY is set on the server and in the store builds (without it no claim can ever count as "in the app"); tier_up hands the lowest rung's product instead of points. The banner text is free text: if you change the bonus points, change the text too. | Safe. |
| Result card seconds / idle close seconds | How long the cabinet shows the QR (25) and when a walk-away innings is closed (60). | Safe. |
Per-machine things (driver, slots, stock) live on the Vending page; a sponsor's own ladder and rung budgets live in the creative editor in the portal.
The catalogue (Products tab): name, kind, cost (what the house pays, used for liability), image URL, points cost (blank = not buyable with points), active. Products are never deleted; deactivating one that a live ladder pays is refused with the campaigns named, so change the rung first. Renaming a product renames it on every screen, claim page and wallet at once.
11. First day with vending in a mall
- Vending → add the machine with the right venue and its "Where it stands" line; pair the box; selftest on the box passes with the real serial port.
- Fill the slot map from the machine's actual layout; set prices to what the machine charges; stock to the real count.
- Rewards settings:
fulfilment.default=vendingnow (it wascounterwhile there was no machine); check the ladder's five products are all stocked in some slot; set each rung's daily budget in the creative if the sponsor has a cap; press Check figures and read the liability line. - Do one vend yourself: play the cabinet, claim on your phone, type the PIN, watch the drop, confirm the slot's stock went down on the Vending page and the request row says vended.
- Pull the box's power mid-vend once (the dispute drill): within 5 minutes you should get the "needs checking" message; resolve it as failed; confirm the coupon is usable again.
- Note the machine's price units (rupees or paise) in its notes for the next technician.
12. Messages ops receive
| Message | Means | Do |
|---|---|---|
| A vend on X needs checking | no confirmation within 90 s, a second failure on one coupon, or a machine vended a coupon the counter had redeemed | §6 |
| Points below zero for wallet … | a points balance went negative (should never happen; the nightly check found it) | tell the engineer; the wallet id in the link is all we hold |
| X is offline | no heartbeat for 15 minutes | §7 |
| X: slot A1 is empty | a slot a live campaign pays from hit 0 (one message per event — you never also get the "is low / is empty" one for it) | refill, or change the rung's product |
| X: slot A1 is low / is empty | the slot crossed the low line (§4); a slot already "low" that runs out tells you once more, "is empty". The refillers were messaged too — unless it was you typing the number in refill mode, or the machine is revoked / not yet paired | check the Today panel's alert row ("2 slots low on X — Ramesh messaged 14:02"); refill |
All of them arrive in the ops bell (/ops/notifications) and by email when email is set up.
13. Refillers
A refiller is the person who actually carries the boxes: a mall staffer, a vendor's runner, or you. They do not log in; they get a WhatsApp (or an email) that says what to bring where.
Vending → Refillers tab (per venue):
- Add: name, phone (10 digits or +91…) and/or email, channel (WhatsApp, email or both), and optionally which machines they cover (none ticked = every machine at the venue). The box "This person agreed to get refill alerts on this number" must be ticked — ask them first; it is their phone number and we record the date they agreed. Changing to a new number asks for the tick again.
- The number is stored encrypted, shown back only as
+91••••••1234, never in any report or log, and Remove deletes it for good (the alert log keeps only the name). - Send test sends that one person the current list right away (even when nothing is low: "all slots are stocked"), so you can check the number works before you need it. It goes the real route: a contact who is switched off, or who does not cover that machine, is refused with the reason — switch them on or tick the machine first.
- Send refill list now on a machine card sends everyone who covers that machine the current list — no waiting, no throttle; it asks you to confirm. Each ops login gets ten of these an hour (test or real): every one costs a WhatsApp message.
- The alert log under the list shows every message: who, which machine, what it said, and a
chip — sent, failed (the provider's reason), or not configured. If the tab shows the
persistent note "WhatsApp is not configured on the server (WHATSAPP_PROVIDER). Alerts are logged
only.", nothing is leaving the building: ask the engineer to set
WHATSAPP_PROVIDER=aisensyand get thefreebee_refill_alerttemplate approved (three fields: machine, venue + where, slot list). Email needsEMAIL_PROVIDER=resend.
What the message says: "Freebee refill: Atrium snacks at Inorbit Mall (ground floor, by the food court) — 2 slots need stock: A1 Cold drink 2 left, B3 Chips EMPTY. Reply done when refilled." Up to eight slots are listed, then "+k more".
When it goes out:
| When | Kind | Rule |
|---|---|---|
| a slot crosses the low line (§4) because of a vend, the machine saying "sold out", or the 5-minute sweep | low / empty | one message per refiller per machine every 6 hours at most, listing every low or empty slot on that machine at that moment — more slots crossing inside those 6 hours add nothing, except that a slot running EMPTY gets through once even if a "low" went out minutes ago |
| you type a stock at or below the line in refill mode | — | nobody is messaged (you already know); the ops bell gets the "is low" row |
| every morning 09:30 IST | digest | one message per refiller per machine that still has a low or empty slot (the scheduler job refill.digest; Today → Background jobs → Run now for a dry run) — skipped for anyone who already got a message about that machine in the last 2 hours, and it counts as that person's one message for the next 6 hours |
| you press the button | manual | at once, every time (ten per hour per ops login) |
"Reply done" is for the person, not for us: the machine's stock is updated by whoever does the refill in refill mode (§4), which also clears the low state and re-arms the alert.
14. Reading the spend numbers
Every figure that says ₹ comes from one rule: when a prize leaves (machine drop, counter hand-over, or you resolving a dispute as "vended"), the product's unit cost at that moment is written on the coupon and never changes. Editing a product's cost later changes what the next hand-over costs, not what the past ones cost. A prize with no catalogue product (an old run-band win, a sponsor's own counter gift) carries no cost and is never counted as our spend.
- Today panel → "Spent today":
₹X= everything handed over since midnight IST (machines and counter), with "n vends · m counter · ₹Y last hour". Refreshes within a minute. - Vending page machine card: "₹200 today · 20 drops · ₹40/hr last hour" — that machine's drops since midnight IST and in the last 60 minutes. Refreshes every 5 seconds while the page is open. "20 cokes in an hour at ₹10 = ₹200" is exactly this line.
- Rewards → Giveaways tab: which screen and which game gave how many of each gift, for a day
range (today, yesterday, 7 days, 30 days, custom), grouped by screen / game / rung / product /
machine / hour / day. Columns: Minted (prizes the games promised), Claimed (banked in a
wallet), Redeemed (handed over — split into vended by a machine / counter), Expired
(never claimed in 48 h, or the coupon ran out), Outstanding (promised, not yet handed over, not
expired), Spent ₹ (what the handed-over ones cost us), Liability ₹ (what the outstanding
ones would cost at today's price). A prize the daily limit turned into points counts as claimed
(CSV column
as_points) and is never a liability. The rows are by the time the prize was won; the hourly bar underneath is by the time it was handed over (that is the "₹200 in the last hour" view). Points have their own three numbers in the strip: points minted, points redeemed, and ₹ spent on points-catalogue coupons. Download CSV gives the same table plus a TOTAL line for the accountant. - A day is an IST day everywhere on these pages. Liability uses today's catalogue price because nothing has been paid yet; spend uses the price on the day it was paid.