Counterself-service ordering

A working demo · not a product

Order on one screen.
Watch it land on the others.

A self-service kiosk, a kitchen display and a counter console — three different jobs, one store, one socket. Tap something on the kiosk below and the ticket appears in the kitchen before your finger leaves the glass.

The whole system, in one screen

Each pane below is a separate surface talking to the same store. Open any of them in their own window or on your phone and they stay in step.

The screen a customer taps. Big targets, one decision at a time, ending at a card terminal.

Kitchen displayopen ↗

Read across a hot room from two metres. Tickets age from grey to amber to red; bump them along as they go.

Counteropen ↗

Today’s numbers, the live order feed, and who is currently connected to this store.

How it holds together

One object per store

Every store is a single SQLite-backed Durable Object. It holds the menu, the order queue and every open socket, so there is exactly one place where the truth lives and nothing to keep in sync.

Durable Objects · WebSocket hibernation

The kiosk never sets a price

It sends item ids and quantities. Every price, option surcharge and total is recomputed inside the store object from the stored menu. A tampered client changes nothing — quantities clamp, unknown options are ignored.

server-side pricing · clamped input

An idle store costs nothing

Sockets use the hibernation API, so a store with nobody looking at it is evicted from memory and woken by the next order. Close the tab mid-service and the queue is exactly where you left it.

hibernation · storage-backed queue

Numbers are checked, not believed

Import a menu and a model finds its structure. Every price it returns is then checked against the transcription — anything not literally printed on the menu is not shown as a price, it is flagged for a human.

deterministic verification

Reading a menu

The model finds the structure. It is not trusted with the numbers.

Paste a menu — or photograph one — and Counter turns it into categories, items, descriptions and options. That part works well: layout is exactly the sort of thing a language model is good at.

Prices are a different problem. Menus print them in a right-hand column, far from the item, and every vision model I tested on this stack either skipped them or invented plausible round numbers. So Counter does not take the model's word for it: every price is checked against the transcription, and anything that is not literally printed on the menu comes back marked price not read for you to type in.

Being told “I could not read this one” is worth more than a confident wrong number — especially when the number is what somebody pays.

What it is made ofand what it will not do

Built with

  • Cloudflare Workers on counter.malikakhtar.dev/api/*
  • One Durable Object per store, SQLite-backed, WebSocket hibernation
  • D1 for the store registry, the shared import cache and daily budgets
  • Workers AI — a vision model to transcribe, a small open-weight model to structure
  • Hand-written HTML, CSS and JavaScript. No framework, no build step, no dependencies

Limits, on purpose

  • Stores a visitor builds expire after 7 days; the examples never do

The payment step is simulated and says so on screen. No card is read, no money moves, and there is no payment provider behind it — that part belongs in a real build, wired to a real acquirer.