An AI agent that watches live orders.
Laayendra is a Go service that watches every live order, nudges riders hands-free and escalates step by step. Rules act; the model talks.
Laayendra is the part of Laayo that watches every live order the way a person in a control room would. It notices when food is ready and nobody has picked it up, when a rider's phone has gone quiet, when a trip is taking far too long. Then it does something about it, starting gently.
The name sounds like an AI. Most of it is not. The parts that act are plain rules in Go. The language model handles the talking, and it never makes a decision on its own.
Event driven, with one timer heap
The first rule was that it must not eat the server. A monitor that polls the orders table in a loop costs more than it saves.
So the engine tails the order status history by id every two seconds, and each new row re-judges just that order. Once a minute it looks over the live set as a safety net, for things that don't create history rows, like a phone that stopped sending GPS. Everything else is timers. When a rule says "look at this order again in eight minutes", the engine pushes a timer onto a single min-heap and sleeps until the earliest one is due.
type timerItem struct {
at time.Time
id int64
gen uint64 // the order's generation when this timer was set
}
func (e *Engine) fire(now time.Time) {
for e.timers.Len() > 0 && !e.timers[0].at.After(now) {
it := heap.Pop(&e.timers).(timerItem)
om := e.orders[it.id]
if om == nil || om.gen != it.gen {
continue // the order changed since; a newer timer replaced this one
}
e.judge(it.id, now)
}
}type timerItem struct {
at time.Time
id int64
gen uint64 // the order's generation when this timer was set
}
func (e *Engine) fire(now time.Time) {
for e.timers.Len() > 0 && !e.timers[0].at.After(now) {
it := heap.Pop(&e.timers).(timerItem)
om := e.orders[it.id]
if om == nil || om.gen != it.gen {
continue // the order changed since; a newer timer replaced this one
}
e.judge(it.id, now)
}
}The generation counter saves a lot of bookkeeping. When an order changes, its generation goes up and a new timer is pushed. Old timers stay in the heap and are skipped when they pop, instead of being searched for and removed.
The first rules are deliberately plain: food ready with a rider assigned and not picked up after eight minutes, a picked-up order with no location from the rider's phone for five minutes, a trip still running thirty-five minutes after pickup. A slow kitchen is judged against the restaurant's own real cooking time once there is enough history (the 75th percentile plus five minutes), and against the time it quoted until then. Each rule has a code, so every alert, call and report says which rule fired.
The service runs under systemd with a hard CPU quota of half a core and a memory cap. If it misbehaves, it slows itself down, not the apps.
A ladder built for people on bikes
Riders can't answer calls while riding, so the escalation is hands-free first.
- A short spoken Nepali line through the rider app, with a vibration pattern. Riding toward the restaurant after the line counts as heard.
- If nothing changes, the line repeats once.
- Only when the bike has stopped (speed near zero for about fifteen seconds) or GPS has gone dark: an in-app voice call over VoIP, then a phone call.
- Before pickup, the order can go to another rider: same zone, fresh GPS, under their order limit, nearest first. If any condition can't be met safely, the case goes to a human queue instead.
Every step has its own switch, and a missing setting means off. It went to production with everything off and was turned on one action at a time.
The design sets the bar by how harsh the action is. A nudge needs one signal. A call or a reassignment needs one strong signal that holds for thirty seconds. Anything that would count against a rider needs two independent signals and a clean GPS trace, and in the scoring plan a false accusation weighs ten times as much as a miss. Calls also wait for the bike to stay stopped, so a short stop at a junction doesn't set off a call.
Calls that listen
The calls are real conversations. A small agent takes the call audio from Asterisk, transcribes what the rider says, asks a model for a reply with the case facts (which order, what the data shows), and speaks it back with a Nepali text to speech voice.
What I got wrong on the first test calls:
- It started talking the moment the line connected. It now waits for a "hello", or a short silence, first.
- Sometimes the first audio on the line was the mobile carrier's own recorded announcement. The agent now recognises carrier phrases, hangs up and logs a carrier problem. A carrier message must never count as "the rider ignored us".
- It talked over the rider. It now stops almost as soon as the rider starts speaking.
- The voice sounded thin. Phone audio is 8 kHz. I was asking for 16 kHz audio and halving it by averaging pairs of samples. Asking the text to speech service for 8 kHz μ-law directly, then adding a little gain and a soft limiter, fixed it.
- When the audio froze mid-call, the agent waited forever. A line watchdog now ends an utterance after a second without audio, asks "hello, can you hear me now?" when audio returns, and hangs up after twenty seconds of nothing.
Every call is saved as a stereo recording, rider on one channel and Laayendra on the other, with a transcript. When something goes wrong I listen to it.
Where the model is, and is not
The "why is this order late" line is a template filled from stage timings, not a model call. It is cheaper, it reads the same every time, and it can't invent anything. The model is used where language really matters: understanding a rider's spoken answer and replying naturally.
Riders can also get a short WhatsApp note when a spoken alert doesn't reach them. That path has the same discipline as payments: one message per alert, enforced by a unique key on the alert id and an idempotency key on the send, a cap per rider per day, and a gap between sends.
The first version stored nothing and learned nothing. To check the rules, I replayed them over a stretch of real orders and read the cases by hand. The per-restaurant cooking times came out of that work, and they are now recalculated every hour.
Get new posts by email
One email when something new goes up. No tracking pixels, unsubscribe in one click.