A Rapido rider is mid-trip, navigating, with a passenger on the back. The platform wants the next gig assigned before this one ends, so it fires notification after notification. Each one covers the map. The rider swipes them away one-handed at 40kmph, and somewhere in that stack was the one good offer.
The rider is not being served by any of this. They are being interrupted by a queue that has no ordering, and they are being asked to triage it at the worst possible moment.
Latch is one button. Press it and incoming gig notifications stop appearing on screen. They are captured, ranked, and held. When the trip ends, press it again and you get one list, best offer first.
The problem, stated precisely
Three separate failures are stacked here, and it is worth separating them because only two are solvable from outside the platform.
- Interruption during a safety-critical task. A full-width notification over turn-by-turn navigation, while riding, is a hazard. This is solvable.
- No ordering. Offers arrive in the order dispatch sends them, which correlates with nothing the rider cares about. The tenth notification may be worth twice the first. This is solvable.
- Time pressure on acceptance. Offers expire in seconds. Holding one may mean losing it. This is not fully solvable from outside, and it is the honest constraint the whole design has to respect.
How it works
Android exposes NotificationListenerService, a permission a user
grants explicitly in system settings, which lets an app read posted
notifications and dismiss them. That is the entire mechanism. No root, no
accessibility-service abuse, no reverse-engineered API, no automation of the
target app.
onNotificationPosted(sbn):
if not latched: return # pass through untouched
if sbn.packageName not in watched: return # only the apps the rider picked
fields = parse(sbn) # fare, distance, pickup, surge
if fields is null: return # cannot read it, do not touch it
store(fields)
cancelNotification(sbn.key) # remove from the shade
updatePersistentCard(count, bestSoFar)
While latched, the rider sees exactly one thing: a persistent low-priority notification reading "7 offers held, best ₹142". No sound, no heads-up, no screen coverage. One tap unlatches and opens the ranked list.
Ranking, without a model
Per the house rule, the intelligence goes in the logic. Ranking an offer needs no language model; it needs arithmetic the rider can audit.
score = (fare + surge) / (pickup_km * DETOUR + trip_km)
- pickup_penalty(pickup_km)
- direction_penalty(bearing_to_pickup, rider_heading)
The only number a rider actually cares about is rupees per effective kilometre, where effective distance includes the unpaid ride to pickup. A ₹120 fare with a 4km deadhead is worse than a ₹90 fare with a 400m one, and the platform's own list will happily show them in the wrong order.
Two adjustments earn their place. A pickup penalty that grows non-linearly, because 5km of unpaid riding is more than five times as bad as 1km. And a direction penalty, because an offer that sends you back across a city you just crossed costs you the rest of the hour.
Every weight is visible and adjustable in settings, and each offer shows its computed rate so the rider can disagree with the ranking. A ranking you cannot inspect is just a different opaque queue.
The hard part: parsing
Everything above depends on extracting a fare and a distance from a notification whose text format we do not control and which changes without warning.
The approach that survives: layered extraction with an honest failure mode.
- Try structured fields first.
Notification.extrasoften carries title, text and big-text separately, which is more reliable than the rendered string. - Then locale-aware patterns for currency and distance. Rupee amounts appear as
₹142,Rs 142,Rs. 142, and distances as2.4 kmor2,4 km. - If a field cannot be read with confidence, do not capture the notification at all. Let it through to the rider untouched.
That last rule is the whole safety design. The worst outcome is not a mis-ranked offer, it is a swallowed one. When parsing degrades because the platform changed its copy, the app degrades into a transparent pass-through rather than a black hole. Riders lose a feature; they never lose income.
Pattern definitions ship as a small remote-updatable config so a format change can be fixed in hours without an app-store release.
What it will not do
These are boundaries, not a roadmap.
- It will not auto-accept. Ever. Tapping accept in another app on the rider's behalf is automation of a service under someone else's terms, and it turns a helpful tool into a liability for the person using it.
- It will not talk to any platform API. Nothing is scraped, no credentials are handled, no account is touched.
- It will not hold anything the rider did not ask it to. Latch is off by default and off after every trip.
- It will not send notification content anywhere. Parsing is on device. There is no server, so there is no data to breach or subpoena.
The constraint we cannot design away
Offers expire. If an offer is held for four minutes and expires at ninety seconds, the rider lost it, and they lost it because of our button.
We are not going to pretend otherwise. The mitigations:
- The held list shows a live countdown per offer where an expiry can be parsed, and greys out ones that have almost certainly lapsed.
- An exceptional-offer break-through: if an offer scores above a rider-set threshold, it comes through immediately even while latched. The rider decides what is worth being interrupted for, which is the actual product thesis.
- Auto-unlatch when the trip is likely over, detected from sustained low speed plus a navigation notification clearing.
Whether the exceptional-offer threshold is enough is the question a pilot with real riders has to answer. It may turn out that holding is net negative during peak surge and net positive off-peak, in which case the honest product is one that recommends when to latch rather than one that latches by default.
Build shape
| Piece | Choice | Why |
|---|---|---|
| Platform | Android only, Kotlin | iOS has no notification-listener equivalent. This product cannot exist there. |
| Capture | NotificationListenerService | The one sanctioned API for this. No root. |
| Storage | Room, on device, 24-hour retention | No server means no privacy surface. |
| UI | Compose, single screen, very large tap targets | Used with gloves, in sunlight, at speed. |
| Config | Remote JSON for parse patterns only | Format changes get fixed without a release. |
| Distribution | Play Store, plus a signed APK | Same posture as BLOKD and Aperture. |
Notification-listener access is a sensitive permission and Play review will ask what it is for. The answer is specific and defensible: the app displays and reorders the user's own notifications from apps they select, on device, and transmits nothing. That is a documented, permitted use.
Status
Scoped, not started. The first milestone is not the app, it is a two-week capture study: log gig notification formats from real riders with consent, and find out whether fare and pickup distance are reliably extractable. If they are not, the ranking is guesswork and the product does not deserve to exist. Everything else is downstream of that answer.