Courier Signals
Every other delivery service worked out how to move a trip forward without asking. We made couriers tap buttons — arrived, picked up, arrived again — and built an entire platform on top of what those taps claimed. Courier Signals is what replaced them: the phone's own sensors, fused through a filter over the trip's state machine, until the taps were confirmation rather than evidence.
The Tap Tax
A Grubhub delivery moved through its lifecycle because a courier pressed a button. Arrived at the restaurant — tap. Picked up the food — tap. Arrived at the customer — tap. Each tap wrote a timestamp, and that timestamp was load-bearing: it drove the diner's tracking page, the ETA shown at checkout, the kitchen start time under just-in-time dispatch, courier pay, merchant scorecards, and every operational report anyone made a decision from.
The competition had largely stopped asking. Their couriers tapped less, or not at all, and the trip advanced anyway. Ours were doing manual data entry at 40 miles an hour, in the rain, holding a bag of pho — and we were treating the result as ground truth.
Taps fail in three ways, and only one of them is a bug:
- They're late. The most common case and the most boring. Arrival gets tapped from the counter, pickup from back in the driver's seat. A minute here, ninety seconds there — and every ETA downstream inherits the lag as if it were real.
- They're early. Arrival tapped two blocks out because the clock on the screen is counting and the courier would like it to stop. Pickup tapped before the bag is in hand. Nothing in the app can tell, so the merchant's "food sat for 6 minutes" report is measuring something that never happened.
- They don't happen. Hands full, phone in a mount, screen locked, no signal, didn't feel like it. The trip silently stops advancing, and support gets a call from a diner watching a courier icon that has been "heading to the restaurant" for twenty minutes.
The tempting framing is that this is a compliance problem — write a policy, build a scorecard, nudge the couriers. That framing is wrong, and expensively so. A tap is a request for a human to report a fact the phone already knows. The fix is not to ask harder.
What the Phone Already Knew
The courier app was already collecting more than enough. Not one signal that answers the question — several, each individually inadequate:
- Location, at roughly 1 Hz. Position plus an accuracy radius, which in a downtown core is a polite fiction. See GPS Urban Canyon for why.
- Speed and closing rate. Derived from the same fixes, so it inherits the same noise, but the direction of travel relative to a destination is enormously informative — it is what separates the walk toward the door from the walk back to the car.
- Motion activity. iOS
CMMotionActivityand its Android counterpart, classifying the phone's motion as automotive, cycling, walking, running, or stationary, each with a coarse confidence. Computed on the motion coprocessor from accelerometer data the OS was already collecting, at effectively no battery cost. This was the signal nobody was using. - Geofence crossings. Entry and exit against a radius around the merchant and the drop-off.
Motion activity is the interesting one because it answers a question position cannot. Position tells you the courier is 30 meters from the restaurant. It does not tell you whether they are sitting in a car at the curb, circling for parking, or walking through the front door — and those are three different trip states with three different arrival times. Motion activity distinguishes them directly, and it keeps working when the position fix is bouncing off a building.
Getting out of the car is the single most informative event in a delivery, and
it is invisible to every geofence you could draw. The transition from automotive to walking is the courier committing to the pickup. In a
strip mall or a parking structure it happens 150 meters from the door, long before any fence
fires. On a bike route it is the only arrival signal that exists, because automotive never appears at all.
Why Not Just Geofences
The first version of this is always a fence: 100 meters around the merchant, fire on entry, fire again on exit. It is a good idea and it gets you a long way, which is exactly what makes it dangerous — it works well enough that its failures look like edge cases rather than a design limit.
The failures are not edge cases. They are Tuesday in a dense city:
- A stop at a red light 120 meters from the restaurant is geometrically identical to parking there.
- A courier who parks in a garage and walks two minutes never enters a door-tuned fence until they are already inside the building.
- Reflected GPS in a canyon puts the courier across the street and back four times in ten seconds, and a naive fence fires on every crossing.
- Exit-as-pickup means the pickup timestamp is really a departure timestamp, so it is structurally late by however long it took to walk back to the car.
Every one of these has a patch — debounce the crossings, widen the radius, add a dwell timer, special-case the ones that still misfire. That pile of patches is a probabilistic model with the probabilities left implicit and hand-tuned. It is cheaper to write the model down.
The Model
A delivery is not a set of independent events. It is a sequence, and it only runs one direction: you cannot pick up before you arrive, and you cannot hand food over before you have it. So the trip is a chain of hidden states —
driving → parked → on foot → inside merchant → on foot → driving
→ parked → on foot → at the door → on foot → departed — and the sensors are noisy, correlated observations of which state the courier is in. That is a hidden Markov model, and the fact that it is one is the entire point: the ordering constraint does most of the work that ad-hoc heuristics do badly.
Each state carries an emission model over the channels: a distribution over motion-activity classes, a distribution over distance to the relevant anchor, a distribution over speed, and a preferred sign for the closing rate. A forward filter maintains the posterior over states as each sample lands. Four decisions in that model earned their place:
- Confidence is a weight, not a filter. A low-confidence
walkingclassification is still evidence; it just moves the posterior less. Thresholding on confidence and discarding the rest throws away most of the signal, because most classifications in a moving vehicle are not high-confidence. - Heavy tails on speed. The obvious choice is a Gaussian, and it is a trap: under a Gaussian, a single 10 m/s reading makes "parked" so unlikely that one bad fix shoves the entire posterior down the chain. GPS-derived speed has fat tails and the likelihood has to say so.
- Different fences for different states. "Inside the merchant" is a tight radius around the door. "Parked" and "on foot near the merchant" get generous ones, because the parking spot can be a garage two buildings over. Pinning all of them to the same fence is the mistake that makes a geofence implementation miss every strip mall.
- Long states are tiled. A geometric dwell prior will happily let the posterior cross a state in a single sample if the evidence just past it is strong enough — which is precisely how a red light near the restaurant becomes a pickup. Splitting the long-dwell states into a few sub-states turns the dwell into an Erlang distribution with a real mode, so a state with a two-minute expected dwell cannot be crossed in two seconds.
The Filter May Revise. The Signal May Not.
The part that took the longest to get right has nothing to do with sensors. It is that the state chain and the events published from it need opposite properties.
The chain has to be allowed to be wrong and recover. A courier who sits at a light 100 meters from the restaurant will pull the posterior toward "parked," and the belief needs a cheap way back when they drive off. Without a reverse transition, the only escape from a wrongly committed "parked at merchant" is forward — and forward, from the merchant, is the pickup. That single missing edge turns one plausible mistake into a cascade that runs the whole trip off the rails.
The published signal has the opposite requirement. Downstream, PICKED_UP is not a
belief; it is a fact that starts a clock, sends a push notification, and pays somebody. It
cannot be un-sent. So the milestone layer sits above the posterior and reads a different
statistic: not "which state is most likely" but P(progress ≥ k), the total mass at or beyond a point in the chain. That
quantity is monotone in k by construction, so thresholding it yields milestones that
can only advance, over a belief that stays free to move both ways.
And because the commit is online, it is made from the prefix. A retrospective decode of the whole trip is more accurate — the demo computes one — but it is not available at the moment the notification has to go out. Every milestone in this system is a decision made with the second half of the evidence still missing, which is why the errors below are almost all positive: a filter that cannot see the future can only be late.
What It Was Worth
The direct win is accuracy, and it is real: milestones land within seconds of the event rather than within a minute of whenever somebody got around to it. Everything computed downstream — ETAs, kitchen start times, courier pay, merchant wait-time reports — gets better for free, because all of it was reading tap timestamps and believing them.
The second-order effects turned out to matter more.
- Taps became confirmation. Once the state machine advanced on its own, a tap was one more observation to fuse rather than the event itself. Disagreement between the tap and the model became a measurable quantity — which made "taps early at this merchant every time" something you could see in a report instead of something you suspected.
- The floor stopped falling out. The failure mode that generates support calls is not a slightly-late timestamp; it is a trip frozen mid-delivery because nobody tapped. Passive inference has no such state.
- It generalized to couriers who cannot tap. The External Courier Gateway needed the same lifecycle events synthesized from a third party's telemetry, and drone deliveries have no thumbs at all. Milestones defined as functions of observed state, rather than as button presses, port to an upstream with no buttons.
The honest limits are visible in the demo too. A long stop near the restaurant still produces an early arrival — the model has no way to distinguish a red light from a parking spot at the moment it has to decide, and neither would you. Pickup is inferred from departure, so it is structurally a little late. And the ablation is not flattering everywhere: with clean GPS, a well-tuned geofence is nearly as good, because position and speed already carry the arrival. Motion activity earns its keep exactly where position stops working — in the canyon, in the garage, on the bike — which is a much narrower claim than "sensor fusion is better," and it happens to be true.
The demo below is a re-creation, not the original code. The state chain, the signal sources, the filter, and the monotone milestone rule are the real design; the road network is synthetic and the numbers are chosen to be legible rather than measured.
Interactive Demo
One delivery, three ways of deciding what happened: what the courier tapped, what a 100 m geofence would have concluded, and what the fused signals infer. The posterior over the trip's states is the raster at the bottom, with ground truth overlaid as a tick on each row. Change what kind of courier this is, make the conditions hostile, and take the motion-activity channel away to see which failures it was holding up.
Buttons get tapped when it is convenient: arrival from the counter, pickup from back in the car. Every downstream ETA inherits the lag.
what the phone is reporting
- motion activity
- stationary
- speed
- 0.0 m/s
- to merchant
- 539 m 0.0 m/s
- to diner
- 1032 m 0.0 m/s
posterior over trip state
- driving to merchant 1.00
- parked at merchant
- on foot → merchant
- inside merchant
- on foot → vehicle
- driving to diner
- parked at diner
- on foot → door
- at the door
- on foot → vehicle
- departed
The marked row is the courier's true state. Milestones commit at P(progress ≥ k) ≥ 0.75.
belief over time
- driving to merchant
- parked at merchant
- on foot → merchant
- inside merchant
- on foot → vehicle
- driving to diner
- parked at diner
- on foot → door
- at the door
- on foot → vehicle
- departed
| milestone | truth | courier tap | geofence only | courier signals |
|---|---|---|---|---|
| arrived at merchant | 1:12 | — | — | — |
| picked up | 4:02 | — | — | — |
| arrived at customer | 5:44 | — | — | — |
| delivered | 6:57 | — | — | — |
| mean absolute error | — | — | — | |
Errors are signed against the moment the thing actually happened. A milestone that has not
fired yet reads —; if it still reads — at the end of the trip, that
method never detected it at all.