Place a Food Order on Uber Eats
Purpose
Drive the Uber Eats web app to assemble a food order for a given delivery address: set the address, find a specific restaurant, open its menu, add one or more items to the cart, and open the cart to read back the order summary (line items + subtotal). This skill is read-only up to — but not including — payment: it stops at the cart / "Go to checkout" boundary and never submits (places) the order, because checkout is gated behind an Uber login and placing an order charges a real payment method. Output is a structured summary of the assembled cart plus a flag indicating that checkout requires authentication.
When to Use
- "Add a Big Mac from the nearest McDonald's to my Uber Eats cart for delivery to <address>."
- Pricing/availability recon: what does item X cost for delivery to a given address, and is the store currently open for that daypart?
- Pre-staging an order for a human to review and submit in the app (the skill hands back the store URL + cart contents).
- Any flow that needs the cart total / fees for an address without actually paying.
Do not use this skill to complete a purchase — it deliberately cannot. A logged-in, payment-capable checkout is a separate, higher-risk skill.
Workflow
Recommended method: browser. There is no usable public/anonymous API — the ordering surface (feed, store, checkout) is served by internal Uber endpoints that require session cookies + an x-csrf-token header and sit behind Cloudflare and reCAPTCHA (see Gotchas). The rendered web app is the only reliable surface.
1. Stealth + residential-proxy session (mandatory)
SID=$(browse cloud sessions create --keep-alive --verified --proxies \
| node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))")
export BROWSE_SESSION="$SID"
--verified and --proxies are both required. ubereats.com fronts every page with Cloudflare + Cloudflare WAF; a bare session risks a 301/challenge on the homepage.
2. Open the homepage and dismiss the cookie dialog
browse open "https://www.ubereats.com/" --remote
browse wait timeout 4000 --remote
# A cookie consent dialog usually renders. Click "Got it" if present.
browse snapshot --remote # find: dialog > button: Got it
3. Set the delivery address (mandatory first action)
The homepage combobox: Enter delivery address gates everything. Type into it, wait for the autocomplete listbox, and click the first matching option:
# From the snapshot, click the address combobox ref, then type (do NOT press Enter yet)
browse click "<combobox-ref>" --remote
browse type "20 W 34th St, New York, NY 10001" --remote
browse wait timeout 2500 --remote # options render ~2-3s after typing
browse snapshot --remote # listbox > option: "20 W 34th St New York, NY"
browse click "<first-option-ref>" --remote
browse wait timeout 5000 --remote
Selecting an option navigates to /feed?diningMode=DELIVERY&pl=<PAYLOAD>. Capture that pl value — it is a reusable, URL-safe base64 of a URI-encoded JSON location object:
{"address":"20 W 34th St","reference":"<uuid>","referenceType":"uber_places","latitude":40.74868,"longitude":-73.98561}
Once you have pl, you can deep-link straight into any subsequent page (feed / search / store) with the delivery address already applied — no need to re-enter it.
4. Find the restaurant
Either use the header combobox: Search Uber Eats, or deep-link directly:
browse open "https://www.ubereats.com/search?pl=<PL>&q=McDonald%27s&vertical=ALL&searchType=GLOBAL_SEARCH&sc=SEARCH_BAR" --remote
browse wait timeout 5000 --remote
browse snapshot --remote
Store results are link elements whose urlMap href is https://www.ubereats.com/store/<slug>/<storeId> (the <storeId> is an opaque URL-encoded token, e.g. VVyxLQxmQrK3Z9dlkZ0-bA). Pick the closest/most-relevant store and click it (or open the store URL directly, appending ?diningMode=DELIVERY).
5. Add a menu item to the cart
On the store page the menu items are links, each containing a nested button: Quick Add.
browse snapshot --remote
browse click "<quick-add-ref>" --remote # opens the item customization DIALOG (not a direct add)
browse wait timeout 3000 --remote
browse snapshot --remote # dialog has option add-ons + button: "Add N to order"
browse click "<add-N-to-order-ref>" --remote # this is the add-to-cart action
browse wait timeout 3000 --remote
- "Quick Add" does not add directly — it always opens the item dialog. The actual add is the
button: Add <N> to orderinside the dialog. - If the item has required option groups (size, protein, etc.), select them in the dialog before "Add N to order" is enabled. Simple items add with defaults.
- Confirm success: the header cart button label flips from
button: 0 cartstobutton: 1 item(then2 items, …).
6. Open the cart and read back the order (STOP HERE)
browse click "<cart-button-ref>" --remote # the "N item(s)" header button
browse wait timeout 3000 --remote
browse snapshot --remote
browse screenshot --remote --path cart.png
Read the cart drawer: each listitem line item (name + price), the StaticText: Subtotal value, and the link: Go to checkout. Emit the structured summary. Do not click "Go to checkout" as part of fulfilling an order — it leaves the read-only zone.
7. (Optional) Confirm the checkout boundary — do not authenticate
Clicking link: Go to checkout while logged out redirects to https://auth.uber.com/v2/?...&redirect=%2Fcheckout — a phone/email + Google/Apple login wall. This confirms checkout requires auth and payment. Never enter credentials, never proceed past this wall, never place the order.
8. Release the session
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Site-Specific Gotchas
- READ-ONLY / NEVER PLACE THE ORDER. Placing an order charges a real payment method. Checkout is behind
auth.uber.comanyway (verified:Go to checkout→auth.uber.com/v2/?...redirect=%2Fcheckout). The skill's terminal state is the cart drawer. Adding items to the cart does not require login; submitting the order does. - Cloudflare + Cloudflare WAF on every page. Pre-run probe returned HTTP 301 with
cloudflare+cloudflare-waf.--verified --proxiesis mandatory; the homepage loaded cleanly with both. A bare session is not reliable. There is also a Google reCAPTCHA notice on the homepage — it did not fire a visible challenge during testing with verified+proxies, but budget for it. - Address is a hard gate. Nothing (feed/search/store) works until a delivery address is set. The homepage combobox is the only way to establish one from scratch.
- The
plparam is the reusable location key. After the address is picked, every URL carriespl=<url-safe-base64>. Decode:decodeURIComponent(atob(decodeURIComponent(pl)))→ the location JSON (address,reference(uber_places uuid),latitude,longitude). Reuse the sameplto deep-link/feed,/search?...&q=, and/store/<slug>/<id>with the address already applied — this is the fastest path and skips re-typing the address. browse typeneeds a focused field + a wait for autocomplete. Click the address combobox first, thentype(do not append Enter — that submits before the suggestionlistboxrenders). Wait ~2–3s for options, then click anoption.- "Quick Add" is a misnomer — it opens the item dialog, it does not add. The real add-to-cart control is
button: Add <N> to orderinside the dialog that Quick Add opens. Naively clicking Quick Add and expecting the cart count to change will fail (observed: cart stayed at "0 carts" until "Add 1 to order" was clicked). - Cart button label encodes state, not a static word. It reads
0 cartswhen empty and<N> item/<N> itemswhen populated — match on the presence of "cart"/"item", not an exact string, when locating it. Go to checkoutis an<a>link, not a button, and its literalhrefin the a11yurlMapis a decoy (https://www.ubereats.com/); the real navigation is JS-driven to/checkout, which bounces to the auth wall when logged out.browseCLI 0.7.2 prints an "Update available: 0.7.2 -> 0.9.6 / Run: npm i -g browse@latest" banner that gets interleaved into command output (includingbrowse snapshot). This is noise, not an error — filter it (grep -v) before parsing. This exact banner stalled an autobrowse inner-agent run: it treated the polluted snapshot as a failure and burned all 30 turns without reaching the menu. A stricter/streaming client should still fall back tobrowse get text body/ directpldeep-links when snapshot output looks corrupted.- Store hours / dayparts limit availability. The store page shows a daypart button (e.g. "Lunch 11:01 AM – 4:59 PM"); some items only appear during their daypart. A closed store or wrong daypart yields a menu with unavailable items.
- Confirmed dead end — no anonymous API. Don't waste time trying to hit Uber's internal feed/store/checkout endpoints cookieless; they require Uber session cookies +
x-csrf-tokenand are Cloudflare-fronted. The rendered web app is the surface.
Expected Output
// Cart assembled successfully, stopped at checkout boundary (primary success shape)
{
"success": true,
"delivery_address": "20 W 34th St, New York, NY 10001",
"restaurant": "McDonald's® (Madison Square Garden (7th Ave & 33rd))",
"store_url": "https://www.ubereats.com/store/mcdonalds-madison-square-garden-7th-ave-%26-33rd/VVyxLQxmQrK3Z9dlkZ0-bA",
"cart": [
{ "item": "Double Cheeseburger", "qty": 1, "price": "$5.49" }
],
"subtotal": "$5.49",
"reached_checkout_review": true,
"checkout_requires_login": true,
"placed_order": false,
"error_reasoning": null
}
// Reached the login/payment wall when confirming the boundary (still not placed)
{
"success": true,
"restaurant": "McDonald's®",
"cart": [ { "item": "Double Cheeseburger", "qty": 1, "price": "$5.49" } ],
"subtotal": "$5.49",
"reached_checkout_review": true,
"checkout_requires_login": true,
"auth_wall_url": "https://auth.uber.com/v2/?...&redirect=%2Fcheckout",
"placed_order": false,
"error_reasoning": null
}
// Restaurant not found for this address
{
"success": false,
"delivery_address": "20 W 34th St, New York, NY 10001",
"restaurant": "McDonald's",
"cart": [],
"placed_order": false,
"error_reasoning": "No store matching 'McDonald's' delivers to this address."
}
// Blocked before the flow could start
{
"success": false,
"cart": [],
"placed_order": false,
"error_reasoning": "address_required | cloudflare_block | recaptcha_challenge | store_closed"
}