ADTC 2026 POS Agent — A Cash Register That Speaks Swahili, Even When the Internet Doesn't

What Inspired This I was standing in a duka in Dar es Salaam, watching a shopkeeper sell rice by the kilo. She had a digital scale, a notebook, and a Nokia feature phone. When I asked how she tracked inventory, she tapped the notebook and said, "Hii ndiyo kompyuta yangu" — "This is my computer." She was not being ironic. She was being accurate. Across East Africa, informal retail is a $700 billion economy, yet the software built for it assumes three things that are rarely true: reliable internet, English fluency, and pre-packaged barcodes. I spent ten days building a point-of-sale agent that assumes the opposite. It runs entirely offline. It weighs loose goods on a USB scale. It understands Swahili natively — not as a translated afterthought, but as a first-class interface language. And it fits inside the RAM of a used laptop. How We Built It The build followed a strict milestone discipline: implement → test → inspect → commit → push → next component. Here is how the architecture hardened day by day. Days 1–2: Foundation and Hardware We scaffolded the repository, built a SQLite schema for unit-aware inventory (cents-as-integers, never floats for money), and wrote the hardware interfaces. For the scale, we built an evdev-based HID reader with an explicit vendor:product allowlist — a hard security boundary. The scale reader retries with exponential backoff on USB disconnect and normalizes raw reports into canonical kilograms: Python UNIT_CONVERSIONS = { ("g", "kg"): lambda v: v / 1000.0, ("kg", "kg"): lambda v: v, } For the barcode scanner, we used OpenCV + pyzbar in a daemon thread. It debounces repeated reads within a 2-second window and degrades gracefully if the webcam is missing — the system must still function with just the scale and manual search. Day 3: The Inference Backend We compiled llama-cpp-python from source against real CPU flags (AVX2, with conditional AVX-VNNI). The model choice was deliberate and locked early: Qwen2.5-3B-Instruct at Q4_K_M quantization. Why not a larger model? Speed is the highest-leverage metric after accuracy in this competition. The scoring function weights S perf ​ at 30%: S perf ​ ∝ tokens per second 1 ​

A 7B model would score higher on benchmark accuracy, but a shopkeeper waiting three seconds for a response is a shopkeeper who stops using the tool. The 3B model generates fast enough that the interaction feels like a conversation, not a computation. We measured real RSS at load time and logged it against our budget. The corrected framework target was: RAM subtotal ​ ≈4.25GB Our measured footprint beat this, leaving headroom for the database, analytics engine, and OS. Days 4–5: The Agent Core — Where the Hard Problems Live This is where most of the engineering risk concentrated. The agent routes every incoming utterance through a three-layer Swahili pipeline: Table Layer Mechanism Latency Coverage L1 Native-speaker-verified noun-class lookup table ≈0ms ∼60% of transaction phrases L2 Fast-path regex for known phrasing patterns ≈0ms ∼25% L3 Qwen2.5-3B with domain glossary + few-shot examples ≈200–400ms Fallback L1 is the secret weapon. Swahili noun-class agreement is deterministic. If a shopkeeper says "Unga wa ngano umepunguka" ("wheat flour has decreased"), the lookup table resolves the subject (unga, class 11), the verb stem (-punguka), and the agreement prefix directly — zero LLM involvement. I wrote the table myself, verified against native speaker intuition, and measured coverage with a 50-phrase probe set. L2 catches patterns like "Nauza kilo moja" ("I am selling one kilo") — structured enough for regex, too variable for a lookup table. L3 is the safety net. It receives a system prompt built from a domain glossary (real shopkeeper vocabulary, not corporate Swahili) and 5–8 few-shot examples. A register guardrail instructs standard coastal Swahili (Kiswahili cha Sanifu), matching the user's dialect if they use informal phrasing first. After routing, a precondition checker runs before any database write. It rejects refunds for nonexistent sales, prevents overselling, and validates units. This runs identically regardless of which layer produced the tool call — you cannot bypass business logic by speaking more colloquially. Days 6–7: API, Frontend, and Analytics We exposed the agent through a synchronous Flask API with threading.Lock discipline on all writes. The frontend is fully vendored — Chart.js, all CSS, all JS served from src/static/vendor/. Disconnect the ethernet cable, reload the page, and it still renders. The analytics_engine skill attaches DuckDB in-process to the same SQLite database and exposes bounded query templates only. No free-text SQL passthrough. The LLM selects a template and fills parameters via a GBNF grammar, same as every other tool. Output is JSON matching a Chart.js-ready schema. Day 8: The Audit That Matters We ran a cold-install audit inside a VirtualBox VM with network disabled, from a fresh git clone on the VM's own filesystem — not a Windows shared folder. This simulates a judge's untouched machine. We took a snapshot after Ubuntu + Guest Additions + the model file were in place. A 30-second snapshot restore beats a 20-minute reinstall if something breaks mid-test. We re-measured the full RAM table, confirmed no thermal throttling under 30-minute sustained load, and tagged the commit gate1-base. Feature freeze. No further changes after this point. Day 9: The Story, Not Just the Code We wrote REPORT.md problem-first, recorded a 2-minute demo video, and rehearsed the Gate 2 Q&A bank with live artifacts ready: RAM arithmetic in real time, unplug-the-webcam demo, walk through a Swahili sale showing the lookup table resolving a real sentence, debug_graph.py live, SHA-256 startup check, reject-a-fake-refund test. What I Learned

  1. Native Fluency is a Systems Advantage Being a native Swahili speaker was not a "soft skill" in this project. It was a hard systems optimization. Every noun-class entry I verified in the lookup table saved milliseconds of inference latency and reduced model hallucination risk. The glossary I wrote contained disambiguation notes that no generic corpus captures — e.g., "pishi" can mean a plastic bag (container) or a unit of volume depending on the verb stem. Encoding that context in the system prompt improved L3 accuracy measurably.
  2. Offline-First is a Design Philosophy, Not a Fallback Building for zero connectivity forced better architecture. Every dependency is vendored. Every wheel is pre-compiled. The frontend has no CDN references. The model is local. The database is local. The result is not a "degraded mode" — it is the only mode, and it is robust because there is no network failure path to handle.
  3. Measurement Beats Estimation We caught a KV-cache miscalculation in the Master Framework that would have caused an OOM kill on the target hardware. The fix came from measuring real RSS with debug/profiler.py, not from reading model card specs. In constrained environments: theoretical_RAM  =actual_RSS The gap is where projects die. We measured, we corrected, and we logged the real numbers. Challenges We Faced The 10-day cap. The original framework assumed 20 days. We compressed it by locking decisions early (model choice on Day 1, no dual-model bake-off) and treating the reserve day as a real buffer, not a planning fantasy. Day 4 (agent core) was the tightest — reviewing a bad OpenHands draft costs hours, not minutes. We budgeted for that overrun rather than pretending it wouldn't happen. Hardware uncertainty. We did not have the final target scale during development. We built MockScale with the exact same (quantity, unit) interface as HIDScale, so the entire business logic layer could be tested without hardware. When the real scale arrives, we only need to implement _read_raw_report() — the rest of the system is already validated. Thermal throttling. A 3B model on a laptop CPU generates heat. We implemented adaptive thread-count reduction via psutil temperature polling. If core temperature climbs, n threads ​ drops dynamically: n threads ​ (t)={ n threads ​ (t−1)−1 n threads ​ (t−1) ​

if T core ​

T threshold ​

otherwise ​

This is not in the model card. It is in the thermal benchmark logs. The "dead-rung" bug. During debug_graph.py verification, we found a control-flow error in the escalation ladder where a failed LLM turn could skip menu mode and jump straight to guarded templates — losing user context. We caught it because Day 4's review step was non-negotiable, not compressed. Why This Matters The shopkeeper in Dar does not need a dashboard. She needs a cash register that understands "Nauza kilo moja ya mchele, bei shilingi elfu tatu" without asking her to switch languages, find a barcode, or wait for a cloud API to respond. We built that. It weighs 4.25 GB in RAM, speaks Swahili at three speeds, and works when the internet doesn't. That is not a prototype. That is a tool.

Built With

Share this project:

Updates

Submission history