IPOT POS · Platform demo · Kaihuan Huang

Operate the restaurant, watch the backend

The point-of-sale platform I led at IPOT, reimplemented in the browser in IPOT's own design language. A cashier terminal, a kitchen display, a guest app and the Nalu assistant run against the same in-page services. Every tap becomes a request through Router → Service → Model, one transaction each, and the trace below shows exactly which rows it wrote.

Architecture and money rules from the POS work I led at IPOT, a team-built platformIndependent reimplementation · all data is syntheticType stands in for IPOT's licensed faces

1 · Run the restaurant

Play the tour, or drive it yourself: tap a table, build the order, send it to the kitchen, take payment, refund a line, void a bill. The guest phone is on the selected table and signed in by phone number; the kitchen screen gets its tickets over WebSocket. The Bookings tab shows what the Reservation API holds; the Requests tab shows guest requests and Nalu's refund proposals.

Backend trace · newest first

Nothing yet. Tap a table on the cashier terminal.

2 · Talk to Nalu, the guest assistant

Nalu is the bilingual (EN / 中文) guest chat at the edge of the platform. Rules classify the intent and read every field; the booking runs only after a bare “yes”; every tool is an HTTP request into the POS (read-only) or the Reservation API, with an Idempotency-Key on the write. A refund is a proposal until a manager approves it on the terminal. In pre-launch testing the model is qwen2.5:7b served by Ollama on the restaurant's own GPU box, so guest messages never leave the network; it gets at most one call per turn, to classify the intent and fill fields the rules missed, and every value it returns is re-validated before it is used. Here the model is off until you connect a local Ollama below; the rules alone carry the whole flow.

What the agent may and may not do

  • Read the POS: GET /menu-items, GET /orders/{id}. No ORM import, no write methods, no guest token stored or sent to inference.
  • Write the POS once: POST /customer/guest-requests, the same public endpoint the QR app uses, keyed by the table session.
  • Book through the Reservation API: POST /api/v1/reservations with an Idempotency-Key, after a bare yes. A full slot comes back 409 and the agent offers open times.
  • Propose a refund, never move money: the proposal passes the same checks the refund endpoint applies, then waits on the terminal's Requests tab. Over $50 needs a manager.
  • Never overclaim: replies about refunds and bookings are fixed templates; the code, not the model, decides what the reply may say.
  • The model, when connected: one bounded call per turn (about 150 output tokens, 10 s timeout, invalid JSON counts as no result). Its intent is used only where the rules matched nothing; its fields only where the rules found nothing, and only if they survive re-validation against the guest's own words.

Local model · off

Nalu's model runs on the restaurant's NVIDIA DGX Spark through Ollama: about 47 tokens/s warm, about 10 s on a cold first call, requests served one after another. Nothing is sent to a cloud API. The flag CUSTOMER_CHAT_USE_LLM ships off; the deterministic path is what the guest gets when it is off or the model fails. If you run Ollama, connect it and watch the model's reading next to the rules. Nothing leaves your computer.

# once, then restart Ollama
ollama pull qwen2.5:7b
OLLAMA_ORIGINS="https://kaihuan-huang.github.io" ollama serve

3 · The platform

Five clients, one FastAPI service in three layers, one PostgreSQL, and two neighbouring services with a hard boundary. Click a box to see what it does and which parts I built.

I built or ledTeam-built; I added parts or the contractTeam-built; I integrated with it

Pick a box

Routers validate input and shape responses. Services hold every business rule and own the transaction. Models are SQLAlchemy tables migrated with Alembic. Nothing below that line is reachable from a router.

The entry points

Every way something outside the POS gets in or out, what guards it, and where this page shows it. “Show me” runs the real request in the trace.

Entry pointDirectionGuardOn this page
Guest QR app → POS guest requestspublic POSTkeyed by table session only; dedupe; 3-minute SLAPhone “Call a server”
Nalu → POSread-only GETno ORM, no write methods, no stored guest token“What's on the menu?”, “How much is my bill?”
Nalu → Reservation APIPOST with Idempotency-Keybare-yes gate; same key + same payload replays, different payload 409Book a table, then
Reservation API → Twiliooutbound SMS + status webhookOTP via Verify; templates only; delivery status by callback, never guessed
Reservation API → Revenue Agentoutbound callbackUTM and click ids on the booking; the booking never waits on marketingAppears on every booking as an outbound call in the trace
Reservation → POS floorstaff actiona reserved table seats only with its reservation code; the bill carries the codeTerminal “Bookings” tab: confirm, reserve a table, seat the party
Nalu refund proposal → POS refundmanager approvalsame checks as the endpoint; approval is the only path that moves moneyTerminal “Requests” tab
Kitchen display, iOS stationWebSocket rooms; API contracttickets numbered by the API; contract doc answers the station's asksKitchen device; platform map

4 · Settlement: four calls, or one transaction

Before my change, the till settled a bill with four requests: capture the payment, close the bill, move the table, write the drawer. Each committed on its own. I replaced them with POST /orders/{id}/settle, which does all four inside one transaction. Pick where the process dies and run both.

Before: four calls from the till

Not run yet.

After: one settle transaction

Not run yet.

Three holes the same change closed

5 · Try to break it

Each button runs against a fresh, fully settled shift. The service answers with the status code and error code the real API uses, and the transaction is rolled back.

6 · Tests

Money rules, price snapshots, kitchen rounds, guest requests, the crash behaviour of settlement, the Reservation API's idempotency and callbacks, and Nalu's gates, run in your browser against the code on this page.

7 · Who built what

The platform is a team effort. Commit hashes are in the IPOT repositories; the demo reimplements the behaviour, not the code.

AreaMineTeam
Money flowAtomic settlement, tips on payments, refunds with reason and approver, audited void, cash-drawer movements (6de5698, 7eb27be, b3c7846)Payment-intent model, PIN login, shifts
KitchenMany rounds per bill, price by the menu the cashier saw, ticket numbers owned by the API, status derived from every round (a9814b2)KDS screen, WebSocket rooms, printing
GuestsGuest-request inbox with dedupe and a 3-minute SLA (392e5ac); QR ordering flow with payment step; phone sign-inQR app shell, menus, i18n
Floor and staffTable triage with Postgres persistence (97c8270); scheduling and availability (0975dec); cashier rotaFloor plan, table model, iOS station
DeliveryiOS API contract answering the station's 11 asks (e520b80); staging CI; diverged Alembic heads repaired; a long-red test suite brought back to green (7fc408c)MVP, Docker stack, deployment
NeighboursReservation API from the first commit, in production; Nalu booking flow v2 (pre-launch)Nalu as a separate service: scaffold, read-only POS grounding

Not shown here and not claimed: real card capture (simulated on the main branch), receipt printing, production deployment. For the tamper-evident audit trail behind these events, see the ledger page.