Domain skill
doordash
Markdown synced from browser-harness domain skills.
- Host
- doordash
- Files
- 1
Agent prompt
Use this skill
Copy this prompt into your coding agent to make it enable browser-harness domain skills and read this exact domain folder before automating.
Set up https://github.com/browser-use/browser-harness for me if it is not already installed. If setup is needed, read `install.md` first to install and connect it to my real browser. Then read `SKILL.md` for normal usage and always read `helpers.py` because that is where the browser-harness functions are. Enable domain skills if they are not already enabled by setting `BH_DOMAIN_SKILLS=1` for browser-harness. Use the `doordash` domain skill from `agent-workspace/domain-skills/doordash/`. Read every markdown file for this domain before inventing an approach: - agent-workspace/domain-skills/doordash/search-and-product-pages.md Use those domain-skill notes to complete my task for `doordash` in my real browser. When you open a setup, verification, or task tab, activate it so I can see the active browser tab.
Skill contents
What the agent will read
cross-store product search, product pages, store info (logged-in Chrome)
search-and-product-pages.md
- The contributor tested doordash.com on 2026-09-26 through browser-harness with a logged-in desktop Chrome session. The contributor reports approximately 600 searches and 100 product pages. Each request uses in-page...
- ---
- ⚠️ The search page never calls its search API from the client. /search/store/<query>/ is SSR;
- hooking window.fetch and typing in the search box only shows autocompleteFacetFeed and externalStores. The results come from searchWithFilterFacetFeed, executed server-side. Its query document is embedded in the page's...
- ⚠️ products-style fields are not where items are. Item cards are FacetV2 nodes with
Show full markdown
The contributor tested doordash.com on 2026-09-26 through browser-harness with a logged-in desktop Chrome session.
The contributor reports approximately 600 searches and 100 product pages.
Each request uses in-page fetch() from an open doordash.com tab.
The requests use the existing session and delivery address.
Traps first
- ⚠️ The search page never calls its search API from the client.
/search/store/<query>/is SSR; hookingwindow.fetchand typing in the search box only showsautocompleteFacetFeedandexternalStores. The results come fromsearchWithFilterFacetFeed, executed server-side. Its query document is embedded in the page's RSC payload as a GraphQL AST ({"kind":"Document",...}next to"variables":{"cursor":"","filterQuery":"","query":"<q>",...}), so print the AST back to a query string once and POST it yourself (below). It works as-is: no persisted-query hash. - ⚠️
products-style fields are not where items are. Item cards are FacetV2 nodes withcomponent.category == "card.retail_item"; the data is the JSON string incustom. - ⚠️ The product-page endpoint answers 502 without a cursor in its "item first" form, and only the "Get it now" section's cards carry one. The cursor can be built (see below).
- ⚠️ browser-harness
js()times out after 5s. A batch of searches takes longer: start an async job that stores its result onwindow, return immediately, then poll with shortjs()calls. - A handful of
HTTP 429(HTML body, not JSON) appeared at 3 concurrent requests over ~600 searches; a retry after ~10–40s succeeds. Treat 401/403 as logged-out / bot check and stop.
Delivery address (what every result is relative to)
POST /graphql/consumer?operation=consumer
{"operationName":"consumer","variables":{},
"query":"query consumer { consumer { id defaultAddress { street city state zipCode printableAddress lat lng } } }"}
Headers for every GraphQL call: content-type: application/json, x-channel-id: marketplace,
x-experience-id: doordash (cookies come from the page). Arbitrary GraphQL documents are accepted.
Search results, store distances and ETAs follow defaultAddress, not the IP: a non-US exit IP with a
US delivery address searched that address's stores. Check the ZIP before trusting any result.
Search across all nearby stores
POST /graphql/searchWithFilterFacetFeed?operation=searchWithFilterFacetFeed
variables: {"cursor":"", "filterQuery":"", "query":"<text>", "isDebug":false, "searchType":""}
Root: searchWithFilterFacetFeed { body { id header {...FacetV2} body {...FacetV2} } page header footer custom logging }
with the usual FacetV2Fragment (id component{id category} name text{title subtitle ...} images events style layout custom logging childrenMap{...}). Walk body[].header, body[].body and every childrenMap.
- Sections (by
logging.item_first_search_section_idof the cards):get_it_now,top_savings,all_results, then one carousel per store ("All stores"). One query returns ~150–700 cards from ~15 stores (Target, Best Buy, Staples, Office Depot, Walgreens, CVS, Lowe's, Home Depot, Kohl's...). The same item repeats across sections: dedupe onstore_id:item_id. - Relevance is loose: "JBL Flip 6" put JLab earbuds first in "Get it now". Filter yourself.
- ~1–4 s per search, 1–2.5 MB of JSON (mostly the
customstrings).
JSON.parse(card.custom) gives:
| field | path |
|---|---|
| ids | item_data.item_id, item_data.store_id, item_data.menu_id, logging.business_id |
| cross-store product id | item_data.dd_sic (urpc_... / ugp_...), same across stores |
| name / store | item_data.item_name, item_data.store_name |
| price | item_data.price.display_string, .unit_amount (cents); list price logging.non_discount_price |
| product rating | logging.item_star_rating, item_num_star_rating, item_num_of_reviews (also price_name_info.default.base.ratings) |
| badges | logging.badge_entries[] → badgeType / badgeUseCase / badgeText |
| link | on_click.deep_link_navigate.path (browse/products/<dd_sic>?cursor=... only in "Get it now") |
Stores name one product differently: for one dd_sic, Best Buy said "JBL Flip Portable
Waterproof Drop-Proof Black Speaker" while Staples said "JBL Flip 7 Portable Waterproof Bluetooth
Speaker". When matching products by name, pool the names of every card with the same dd_sic.
Badge use cases seen on retail items: recently_bought ("700+ recently sold", type social_proof),
percent_discount ("27% off"), loyalty_price, item_promo_canonical, map_item ("Promo excluded"),
stock (high_stock_3p, x_in_stock, low_stock_3p = "Likely out of stock", out_of_stock_3p) and
item_ratings. There is no "Best seller" badge on retail items.
Product page (the modal a result opens)
POST /unified-gateway/retail/v1/pdp
{"screen_type":"SCREEN_TYPE_MOBILE","request_fields":{"_type":"item_first_request_fields","dd_sic":"<dd_sic>","cursor":"<cursor>"}}
{"screen_type":"SCREEN_TYPE_DESKTOP","request_fields":{"_type":"store_request_fields","store_id":"<s>","item_id":"<i>",
"aggregate_store_ids":[],"should_enable_navigate_to_store":false}}
-
product_page[]has one entry per store selling the product (item-first form) or just the one store (store form):product.name,product.price_list[0].price,product.breadcrumbs_category_list.categories[].name(["Target","Electronics","Audio"]),logging.badge_entries,components[](_typebrand,item_price,metadata_entry→badge.ratings.ratings{average,count_of_ratings,count_of_reviews},collapsible_sections→ "Specifications" table with Brand / Model Number / ...). -
store_selector.store_list[]: every nearby store with that product, its price and ETA. -
⚠️ Only the item-first form has breadcrumbs; the store form returns
categories: []and no store list. -
⚠️ Item-first needs a cursor. It is base64url( 4-byte big-endian length + an LZ4 block ) of text:
code{{<store_id>|t|~|~}}|{{<item_id>|<menu_id>|<store_id>|<business_id>|t}}|{GLOBAL_SEARCH_PAGE}|<query>|all_results(real ones list more stores in the second group). An LZ4 block of one literal-only sequence is valid, so no compressor is needed: token
0xF0+ length-15 in 255-steps (orlen<<4under 15), then the bytes. A built cursor returned the full page with breadcrumbs for every store.
Store info
query storepageFeed($storeId: ID!, $menuId: ID, $isMerchantPreview: Boolean, $fulfillmentType: FulfillmentType) {
storepageFeed(storeId: $storeId, menuId: $menuId, isMerchantPreview: $isMerchantPreview, fulfillmentType: $fulfillmentType) {
storeHeader { id name business { id name } address { street city state displayAddress lat lng }
ratings { numRatings numRatingsDisplayString averageRating } distanceFromConsumer { value label } } } }
Works for retail/convenience stores (Best Buy, Target, Staples, Walgreens...), fulfillmentType: "Delivery".
URLs
https://www.doordash.com/convenience/store/<store_id>/product/<item_id>→ redirects to/convenience/store/<store_id>?product_id=<item_id>and opens that product's modal at that store.https://www.doordash.com/browse/products/<dd_sic>→/products/<slug>/<dd_sic>, the cross-store page./convenience/store/<store_id>/item/<item_id>is a 404.