I Reverse-Engineered Zomato’s Food Rescue Feature — Here’s What I Found Inside
嗯,用户让我帮忙总结一下这篇文章的内容,控制在一百个字以内,而且不需要用“文章内容总结”之类的开头。直接写文章描述即可。首先,我需要通读整篇文章,理解其主要内容和结构。 这篇文章主要讲述了作者如何解决Zomato的“Food Rescue”活动中的竞速问题。Zomato会在用户附近有订单取消时弹出优惠窗口,但这个窗口消失得很快,用户很难及时点击。作者因此开发了一个无头监控工具来提前通知自己。 接下来,作者详细描述了开发过程中的各个步骤:拦截流量、分析HTTP请求、发现MQTT协议、寻找MQTT主机、订阅事件通道、处理重复事件以及构建最终的应用程序。整个过程涉及到了Android逆向工程、网络协议分析和实时事件处理。 最后,文章还讨论了Zomato的架构特点,如服务器驱动的UI和MQTT的使用,并提出了未来的改进方向。 现在,我需要将这些内容浓缩到100字以内。要抓住关键点:Food Rescue活动的竞速问题、作者开发的无头监控工具、使用的MQTT协议以及最终实现的通知功能。 可能的总结是:“作者为解决Zomato的Food Rescue活动竞速问题,开发了一款无头监控工具。通过分析HTTP流量和MQTT协议,实时捕捉订单取消事件并推送通知,帮助用户及时抢购优惠。” 这样既涵盖了问题、解决方案和结果,又控制在了100字以内。 </think> 作者为解决Zomato的Food Rescue活动竞速问题,开发了一款无头监控工具。通过分析HTTP流量和MQTT协议,实时捕捉订单取消事件并推送通知,帮助用户及时抢购优惠。 2026-3-11 04:35:28 Author: infosecwriteups.com(查看原文) 阅读量:5 收藏

Jatin Banga

Zomato’s “Food Rescue” is essentially a race condition. Here’s how I built a headless monitor to win it.

Every so often, Zomato throws a pop-up on your screen: a cancelled nearby order offered at 50% off. It’s gone in seconds — claimed by whoever is fast enough, or lucky enough to be staring at the right screen at the right time.

I missed it one too many times. Not because I was slow — because I wasn’t even looking. And that bothered me enough to do something about it.
What started as “I want a notification before the flyer appears” became a weeks-long deep dive into Android traffic interception, MQTT protocol internals, server-driven UI architecture, and Zomato’s real-time event pipeline. This is that story.

The Problem: A Race Condition You Can’t Win Passively

Food Rescue events trigger when a real human cancels an order that is already out for delivery. The deal appears for users within a ~3 km radius of where the delivery person currently is. The moment it appears in your app, it’s also appearing for dozens of other users. Miss the flyer — or accidentally swipe back — and it’s gone.

The official app requires you to be actively watching it. I wanted a solution that didn’t.

My goal: intercept the exact same real-time signals the Zomato app listens to, and send myself a push notification the moment a cancellation event fires — before the app even opens.

Step 1: Intercepting the Traffic

Reverse-engineering a web app is trivial. You open DevTools, go to the Network tab, right-click a request, and copy it as cURL. Done.
Mobile apps are a different problem entirely. Modern Android apps operate across three levels of certificate trust, each progressively harder to break:

Level 1: User Trust: The app trusts any certificate you install in your phone’s settings. This method has been dead since Android 7, where apps stopped honoring user-installed certificates by default.

Level 2 : System Trust: The app trusts certificates pre-installed on the OS (the System Store). To intercept here, you need a rooted device or emulator so you can inject your own certificate into the system store.

Level 3: SSL Pinning: The app hardcodes a specific fingerprint of its server certificate directly in its own code. Even if your certificate is in the system store, the app checks: “Does this certificate’s hash match the one I have stored?” If it doesn’t, the connection is killed immediately.

Interestingly, Zomato’s basic HTTP traffic was interceptable using HTTPToolkit, which injects a certificate into the system store. SSL pinning — if it exists in Zomato’s stack — either wasn’t enforced for these endpoints or HTTPToolkit was handling more under the hood than I initially understood.

I searched the intercepted traffic for keywords like food_rescue and rescue. An hour in, I found what I was looking for.

Step 2: The Home Feed Endpoint

When you open Zomato to a saved address, the app calls:

GET /gw/tabbed-home?cell_id=412037575450XXXXXX&address_id=123456 HTTP/1.1
Host: api.zomato.com
X-Zomato-Access-Token: ...
Accept: application/json
User-Agent: OkHttp/4.12.0

Both cell_id and address_id come from your saved addresses — they’re returned when you list your saved locations in the app.

The response is enormous — thousands of lines of JSON. The reason: Zomato uses server-driven UI. Every button, every action, every layout in the app is defined in this JSON response. The client is essentially just an interpreter. You see things like layout_snippet_v3, timer_snippet_type_4, and so on — the server tells the app what to render and what to do when you tap it.

Buried inside this response, under subscription_channels, was this:

{
"subscription_channels": [
{
"type": "food_rescue_cell_412037575450XXXXXX",
"time": 174XXXXXXXX,
"client": {
"username": "zemqtt",
"password": "secure_password_here",
"keepalive": 900
}
}
]
}

I had no idea what MQTT was. A quick ask to an LLM confirmed: this was an MQTT subscription configuration. The app was subscribing to a real-time event channel. The MQTT host, however, wasn’t in the payload — it was hardcoded somewhere in the application binary.

Step 3: Finding the MQTT Host

This is where things got harder for me.

HTTPToolkit only intercepts HTTP. The garbled traffic I had been seeing in its UI wasn’t a bug — it was MQTT traffic being intercepted and misrepresented. No useful clues there.

I tried decompiling the APK using JADX and searching for strings like *.zomato.com and mqtt. I could see where the MQTT connection was being established in the code, but I couldn’t isolate the actual host — too many search results, too much layering.

So I did something unconventional: I asked Grok to deep-search for anything publicly available about Zomato’s MQTT infrastructure. It surfaced the host: “consumermqtt.zomato.com”.

I still don’t know exactly how it found it. But after trying different protocols and common MQTT port numbers, I was able to connect. Later I discovered the official app actually uses “hedwig.zomato.com” — the other endpoint appears to be an alias.

Step 4: The First Real MQTT Message

Using MQTT Explorer, I subscribed to the topic food_rescue_cell_412037575450XXXXXX. The first message I ever received looked like this:

{
"data": {
"event_type": "order_claimed",
"success_actions": [
{
"food_rescue_order_claimed": {
"identifier": "78xxxxxxxx" # Order ID
},
"type": "food_rescue_order_claimed"
}
]
},
"id": "0f934ff8651fae92d1a95443XXXXXXXXX",
"timestamp": 177XXXXX,
"type": "food_rescue"
}

A few things I learned immediately:

  • Zomato’s MQTT broker retains messages. If the last event was order_claimed, reconnecting 30 minutes later would still show you that message. Same for order_cancelled — if the order wasn't claimed, the cancellation message persists for a while.
  • There are two event types that matter: order_cancelled (the trigger) and order_claimed (the resolution).
  • Subscribing to # (all topics) was blocked — Zomato has access control configured per client.

Step 5: How the App Reacts to a Cancellation

At this point I needed to understand exactly what the app does when it receives an order_cancelled event. HTTPToolkit couldn’t help here since it was blocking MQTT traffic.

Get Jatin Banga’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

I found apk-mitm, which patches the APK, removes the System Trust restriction from the manifest, and recompiles it — so the modified app explicitly trusts user-installed certificates. Combined with mitmproxy, I could now see both HTTP and MQTT traffic simultaneously.

After some waiting, a real cancellation event occurred nearby. The moment it did, the app fired:

POST /gw/gamification/food-rescue/create-cart HTTP/1.1
Host: api.zomato.com
X-Zomato-Access-Token: your_token_here
X-User-Defined-Lat: 28.5355
X-User-Defined-Long: 77.3910
...

With a body built entirely from data already available in the address object. The response — when an active rescue exists — is a deeply nested JSON structure containing the cart ID, parent order ID, restaurant details, number of people watching, and a countdown timer.

{
"response": {
"floating_timer_view_1": {
"click_action": {
"open_food_rescue_bottom_sheet": {
"results": [
{
"timer_snippet_type_4": {
"button": {
"click_action": {
"deeplink": {
"url": "zomato://order?res_id=1912345",
"post_body": "{\"cart_id\":\"cart_888\",\"context\":{\"cart_modification\":{\"ParentOrderID\":\"order_999\",\"ParentCartID\":\"cart_777\",\"CartModificationType\":\"replace\"},\"cart_analytics_data\":{\"number_of_people_watching\":\"15\",\"cart_expiry_timestamp\":\"1740000000\"}}}"
}
}
}
}
}
]
}
}
}
}
...
}

Step 6: The “Pitch Once” Constraint

Here’s the painful part I discovered after wasting a day or two: the /gw/gamification/food-rescue/create-cart endpoint is pitch-once only.

The moment you call it, Zomato marks that you have been shown the offer. Call it a second time — even a second later — and you get a failure response. The server has either flagged your session as already notified, or someone else claimed it faster.

This means: if I called the endpoint programmatically when the MQTT event fired, and then the user opened the official Zomato app hoping to see the flyer — nothing would be there. The app had already “shown” it to my script.

I tried deep linking into the Zomato app from a notification to hand off the session. None of it worked reliably — too time-sensitive, too many variables.

So I accepted a simpler outcome: listen for the MQTT event, send a push notification, let the user open Zomato themselves. The notification would at least give them a head start versus waiting to stumble upon the flyer organically.

Step 7: Deduplication and Cooldown Logic

One issue I didn’t expect: Zomato sometimes fires the same order_cancelled event 3–5 times in rapid succession. Whether this is intentional retry logic or a side effect of their message retention setup, I'm not sure.

The fix was straightforward:

  • Store the unique id from each event.
  • If the script has already processed that ID, skip it.
  • Implement a cooldown window so that even if multiple events arrive for the same cancellation, only one notification goes out.

The first notification is all a user needs. Sending five is just noise.

Step 8: Building It Out

At this point the core logic was working. The remaining questions were about packaging.

Login flow: Rather than asking users to manually intercept and hardcode their access token, I reverse-engineered Zomato’s login flow — phone number, OTP, session establishment — and built it into the tool. This took around 10–15 hours but made the tool self-contained.

Distribution: I packaged the Python logic into a CLI app: github.com/jatin-dot-py/jomato. But asking people to run a Python script on Termux or a personal server felt like the wrong UX.

I tried building a mobile UI with Flet. Eight hours later, nothing usable.

So I ported the entire thing to Kotlin. The prompt I gave the LLM was precise: “Copy this Python script exactly. Same headers, same business logic. At the network level, no one should be able to tell whether it’s Python or Kotlin.” The Kotlin app came together in under 24 hours: github.com/jatin-dot-py/jomato-mobile.

What This Reveals About Zomato’s Architecture

A few things stood out from this investigation worth noting independently:

Server-driven UI at scale: Zomato’s entire frontend is orchestrated from the server. The client app is a generic renderer. This lets them ship UI changes without app updates, but it also means the entire payload — including internal action endpoints, deeplinks, and analytics — is exposed to anyone who can read the JSON.

MQTT for real-time events: Using MQTT for features like Food Rescue is a sensible architectural choice. It’s lightweight, persistent, and works well for broadcast scenarios. The credential exposure in the home feed response (username and password for the MQTT client) is worth noting — anyone who can read that API response can subscribe to the same topics.

The pitch-once constraint as abuse prevention: The single-call limit on /gw/gamification/food-rescue/create-cart is almost certainly intentional — it prevents one session from camping on the endpoint and blocking other users. But it has the side effect of making the flow incompatible with programmatic interception, which may also be intentional.

What’s Next

A few ideas I haven’t fully explored:

Deep linking completion: There may be a way to start the cart programmatically and hand off to the official app mid-flow. I haven’t found it yet, but I haven’t fully given up.

Shadow account approach: Run a secondary Zomato account that mirrors your address and location. The shadow account handles the MQTT monitoring and the initial cart call. When it fires, your real account opens the app fresh — no “already pitched” state, full flyer experience.

Good enough as-is: The notification-first approach already removes the need to stare at the app. For most users, that’s sufficient.


文章来源: https://infosecwriteups.com/i-reverse-engineered-zomatos-food-rescue-feature-here-s-what-i-found-inside-f7043d3710ee?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh