jd.com

search-products

Installation

Adds this website's skill for your agents

 

Summary

Search JD.com (Jingdong) for products by keyword and return structured results — title, price in CNY, JD-self-operated flag, review count, stock status, canonical URL — with brand / price-range / sort filters. Read-only. Requires an authenticated JD context or a China-resident proxy: foreign IPs hit a hard login wall on every product subdomain.

FIG. 01
FIG. 02
FIG. 03
SKILL.md
254 lines

JD.com Product Search

Purpose

Given a keyword (and optional brand / price-range / sort filters), return a structured list of matching products from JD.com (京东 / Jingdong, China's largest electronics retailer): product title, current price in CNY (¥), JD-self-operated flag (自营), star rating, review count, stock status, and the canonical https://item.jd.com/<sku>.html URL. Read-only — never adds to cart, never checks out, never edits an account.

When to Use

  • Price-monitoring an iPhone / appliance / electronics SKU on JD's primary Chinese-domestic catalog.
  • Comparing JD-self-operated (京东自营) vs. third-party-seller listings for the same keyword.
  • Bulk extraction of search results across multiple sort orders (sales-descending, price-ascending) for category research.
  • Anywhere the user explicitly asks for jd.com product data and a Joybuy / global.jd.com substitution would be wrong (different SKU catalog, China-export-filtered inventory, separate pricing).

Workflow

Read this section in full before touching a session — JD.com has a hard anti-bot wall for foreign IPs that the rest of this section is built around. Skipping the prerequisites guarantees a login redirect with zero useful data.

Prerequisite — one of these MUST be true, or the skill cannot return data

JD.com fingerprints the request IP and forces every search.jd.com, list.jd.com, item.jd.com, so.m.jd.com, wq.jd.com, and search.jd.hk request from a non-China IP through https://cfe.m.jd.com/privatedomain/risk_handler/03101900/, which immediately issues a 302 to https://passport.jd.com/new/login.aspx. There is no JS challenge to wait out; the redirect fires instantly. Verified 2026-05-20 across us-west-2 and ap-southeast-1 Browserbase regions, with and without --verified --proxies, and with proxies: [{type: "browserbase", geolocation: {country: "CN"}}] (which silently fell back to a US Oregon AWS IP — Browserbase's residential proxy pool does not include China exit nodes).

You need exactly one of:

  1. A persisted JD authenticated context — a Browserbase Context (--context-id <id> --persist) that already holds the cookies for a logged-in JD account (pin, thor, pt_key, pt_pin, unick, flash). The QR code on the login page binds to the JD mobile app, so the human bootstrap is: scan the QR in --debug-url once, save the context, reuse it for every future run. This is the only path the host marketplace currently exposes for JD skills.
  2. A China-resident residential or datacenter proxy wired through proxies: [{type: "external", server: "<host:port>", username: "<u>", password: "<p>"}] in the session-create body. This bypasses both the foreign-IP wall and the login requirement for unauthenticated browsing of search/category/item pages. Browserbase's built-in proxy does NOT count — confirmed-tested CN-geolocation hint via the body returned a US AWS IP.

If neither is available, stop and emit {"success": false, "reason": "auth_required"} — do not navigate search.jd.com and pretend to parse a result; the page is the login form and any "extracted" data is hallucinated.

1. Stealth + proxy session

SID=$(browse cloud sessions create --keep-alive --proxies --verified \
        --context-id "$JD_CONTEXT_ID" --persist \
        --region us-west-2 \
        | node -pe "JSON.parse(require('fs').readFileSync(0,'utf8')).id")
export BROWSE_SESSION="$SID"

--verified (humanlike fingerprint) + --proxies is mandatory even with a logged-in context — JD also fingerprints UA/canvas/WebGL and a bare Chromium gets booted on first navigation. If using an external CN proxy, swap --proxies for --body '{"proxies":[{"type":"external","server":"...","username":"...","password":"..."}]}'.

2. Warm the session on the consumer homepage

browse open "https://www.jd.com/?gtm_test=us" --remote
browse wait load --remote
browse wait timeout 2000 --remote

The ?gtm_test=us query string is required from a non-China IP. A bare https://www.jd.com/ 302-redirects to corporate.jd.com (the English investor-relations site, a completely different page with zero products). The gtm_test=us parameter keeps the routing on the Chinese consumer site (京东(JD.COM)-正品低价、品质保障...). Verified — this trick still gives the consumer page even on a fresh foreign-IP session before any cookies are set.

After this load, the session has the cookies JD's edge expects: __jdv, __jdu, areaId, ipLoc-djd, 3AB9D23F7A4B3C9B, wlfstk_smdl, shshshfpa, shshshfpb, sdtoken. These alone are NOT sufficient to bypass the search-page wall — they only get you past the homepage-level edge check. The login cookies from step 0's context are what actually open search.jd.com.

3. Build the search URL

https://search.jd.com/Search
    ?keyword=<URL-encoded-keyword>
    &enc=utf-8
    &psort=<sort-code>             # see table below; omit for relevance (default)
    &ev=<filter-expr>              # see filter syntax below; omit for unfiltered
    &page=<2*pageNumber - 1>       # JD uses odd-numbered pages (1, 3, 5, ...)
    &s=<startIndex>                # 1 for page 1, 31 for page 2, 61 for page 3

psort (sort order) — JD's psort codes (verified against the desktop search UI):

psortSort
omitted or 0Comprehensive / relevance (综合) — JD's default ranking
3Sales descending (销量)
4Price ascending (价格升序)
2Price descending (价格降序)
5Reviews / rating descending (评论数)
6Newest (新品)

ev (filter expression) — multiple filters are concatenated with %5E (URL-encoded ^), and each filter has the shape <facet>_<value>:

  • Price range: exprice_<min>-<max> (e.g. exprice_3000-5000). Use 0 for an open lower bound.
  • Brand: exbrand_<brandName>brandName is JD's canonical brand string in Chinese OR English, depending on the brand. Apple is Apple / 苹果; Xiaomi is Xiaomi / 小米; Huawei is HUAWEI / 华为. Brand names with spaces must be URL-encoded. Multiple brands: chain with another ^exbrand_.
  • JD-self-operated only: 4_4 (the facet 4, value 4 — verified by clicking the "仅看京东自营" toggle).

Example with iphone keyword, sort by sales, Apple brand, ¥3000–5000:

https://search.jd.com/Search?keyword=iphone&enc=utf-8&psort=3&ev=exbrand_Apple%5Eexprice_3000-5000&page=1&s=1

4. Navigate and wait for the grid

browse open "$SEARCH_URL" --remote
browse wait load --remote
browse wait timeout 3000 --remote   # JD lazy-loads price/stock via XHR after DOMContentLoaded

Verify you actually landed on the search page, not the login wall:

URL_AFTER=$(browse get url --remote)
case "$URL_AFTER" in
  *passport.jd.com*|*cfe.m.jd.com/privatedomain/risk_handler*)
    echo "AUTH_REQUIRED: context cookies expired or proxy IP flagged" >&2
    exit 2
    ;;
esac

If you see passport.jd.com/new/login.aspx — the context cookies expired (JD's thor token has a ~14d TTL); a human needs to re-QR-scan to refresh the context, and this skill should return {"success": false, "reason": "auth_required"}.

5. Extract the product grid

The search results live under #J_goodsList ul.gl-warp.clearfix > li.gl-item, each with a data-sku attribute on the <li> carrying the SKU id. Per-item extraction (DOM selectors — run via browse eval):

Array.from(document.querySelectorAll('#J_goodsList li.gl-item')).map(li => ({
  sku: li.getAttribute('data-sku'),
  url: `https://item.jd.com/${li.getAttribute('data-sku')}.html`,
  title: li.querySelector('.p-name em')?.innerText.trim()
       ?? li.querySelector('.p-name')?.innerText.trim(),
  price_cny: parseFloat(li.querySelector('.p-price i')?.innerText) || null,
  self_operated: !!li.querySelector('.p-icons .goods-icons.J-picon-tips[data-tips*="自营"]')
                 || !!li.querySelector('.p-icons em:has-text("自营")'),
  review_count: (() => {
    const txt = li.querySelector('.p-commit strong a')?.innerText || '';
    const m = txt.match(/^([\d.]+)([万+]?)/);
    if (!m) return null;
    const n = parseFloat(m[1]);
    return m[2].includes('万') ? Math.round(n * 10000) : n;
  })(),
  shop: li.querySelector('.p-shop a')?.innerText.trim(),
  stock_status: li.querySelector('.p-stock')?.innerText.trim() || 'in_stock'
}));

Rating is not in the search grid. JD only shows review count and a percentage-good label ("99%好评率") on the grid; the numeric star rating (1.0–5.0) is exposed only on the item-detail page (item.jd.com/<sku>.html#summary-comment .comment-percent). If the caller wants a rating, do a follow-up fetch per SKU (expensive — N+1) or return the percentage-good as a proxy.

6. Paginate

JD uses an odd-numbered page parameter (1, 3, 5, ... not 1, 2, 3, ...) because each "page" is two halves of a 60-item AJAX-paginated unit. For page N of results: page=2*N-1, s=30*(N-1)+1. Total result count is in #J_resCount on first load.

7. Release the session

browse cloud sessions update "$SID" --status REQUEST_RELEASE

Site-Specific Gotchas

  • Foreign IPs are hard-walled. Every product-browsing subdomain (search.jd.com, list.jd.com, item.jd.com, so.m.jd.com, wq.jd.com, search.jd.hk) issues a 302 to https://cfe.m.jd.com/privatedomain/risk_handler/03101900/?returnurl=...&evapi=hold_search_php which immediately bounces to https://passport.jd.com/new/login.aspx from any non-China IP, with or without proxies/stealth. Verified across us-west-2 and ap-southeast-1 regions. Without authenticated cookies or a real CN-IP, the skill is non-functional. Do not "guess" at output shapes.
  • https://www.jd.com/ redirects to corporate.jd.com from non-China IPs — use ?gtm_test=us to land on the Chinese consumer homepage. This is the ONE page that renders without authentication. It's useful for warming cookies and visually confirming the IP is not banned outright, but the search box on it still POSTs to search.jd.com and bounces to login on submit.
  • Browserbase's built-in proxy is NOT a China IP. proxies: [{type: "browserbase", geolocation: {country: "CN"}}] silently routes to a US AWS Oregon IP (verified 52.41.230.44 → ipinfo: Boardman, Oregon, US). To get a real CN exit node, you need an external proxy via proxies: [{type: "external", server: "...", ...}].
  • The mobile JSON API https://api.m.jd.com/?functionId=... requires a signed body parameter (uuid, body SHA-256 sig, client, clientVersion, area, eid, fp, appid=jd_app_android). Unsigned calls return {"code":"1","echo":"no access"}. Recovering the sign function requires reverse-engineering JD's t1 algorithm from the mobile app — out of scope for a marketplace skill. Don't waste time on api.m.jd.com.
  • The AJAX scroll endpoint https://search.jd.com/s_new.php?keyword=...&page=2&s=26&scrolling=y returns a polite Chinese error from foreign IPs: {"code":"0","message":"success","body":{"errorCode":"601","errorReason":"大促异常火爆,已优先为您接入快速通道,稍安勿躁,请返回上一页重新尝试下~~~~"}}. Translation: "promotion-traffic excuse → please go back". It's anti-bot, not actual traffic. Don't retry — it won't clear.
  • global.jd.com and joybuy.co.uk are different catalogs. global.jd.com (京东全球版) and Joybuy (jd.com's international shopping arm, redirects to joybuy.co.uk for US IPs) carry a heavily-curated subset of the Chinese SKU catalog filtered for cross-border export, with different prices and SKU IDs. If the user asked for jd.com, do NOT silently fall back to these — return auth_required instead. The skill marketplace has separate listings for global.jd.com if/when that's the target.
  • page is odd-numbered (1, 3, 5, ...), s is the start-index (1, 31, 61, ...). A naive page=2 returns the same 30 items as page=1 — confusing but consistent with how JD's AJAX paginator increments.
  • Rating (1.0–5.0 stars) is not in the search grid. Only the review count (.p-commit) and a percentage-good label (99%好评率) appear. The numeric rating requires a per-SKU fetch of item.jd.com/<sku>.html. If the caller asked for rating, either do the N+1 fetches (each requires the same auth wall) or return the percentage-good as a 0–100 proxy.
  • Self-operated flag. The "京东自营" badge is on <li> via .p-icons .goods-icons[data-tips*="自营"] and/or a textual <em>自营</em> inside .p-icons. Some JD-self-operated listings still ship from a partner warehouse — the 自营 badge is the canonical signal regardless. Third-party shops show .p-shop a text linking to a mall.jd.com/index-<merchantId>.html URL.
  • Prices update lazily via XHR after DOMContentLoaded. The initial HTML often shows ¥0.00 placeholders; wait 2–3s after load before reading .p-price i, or some prices will be missing. Pre-sale (预售) and out-of-stock (无货) items may legitimately show no price — preserve null, don't fabricate.
  • Brand-name canonicalization. ev=exbrand_<brand> requires JD's exact canonical brand string. For mixed-language brands, the canonical is usually the English (Apple, Xiaomi, HUAWEI, Samsung) but some are Chinese-only (格力, 美的, 海尔). Verify by clicking a brand checkbox in the UI once and reading the resulting URL ev segment. Wrong brand string → no filter applied, results returned silently as if unfiltered.
  • Captcha / "滑动验证" (slide-puzzle). Even with valid cookies, JD periodically interposes a slide-to-verify captcha (https://iv.jd.com/...). Browserbase --verified does NOT auto-solve these; the only options are (a) --solve-captchas (additional cost), (b) human intervention via --debug-url, or (c) waiting ~5 min and retrying with a new session. Capture the captcha screenshot and surface it as {"success": false, "reason": "captcha_required"}.
  • Read-only. Never click .btn-addtocart, never navigate to cart.jd.com, never submit passport.jd.com credentials programmatically. Stop at the search-result extraction.

Expected Output

Three distinct outcome shapes:

// Success — results returned
{
  "success": true,
  "keyword": "iphone",
  "sort": "sales_desc",
  "filters": { "brand": "Apple", "price_min": 3000, "price_max": 5000, "self_operated_only": false },
  "total_results": 8742,
  "page": 1,
  "products": [
    {
      "sku": "100012043978",
      "title": "Apple iPhone 15 (A3092) 128GB 黑色 支持移动联通电信5G 双卡双待手机",
      "price_cny": 4499.00,
      "url": "https://item.jd.com/100012043978.html",
      "self_operated": true,
      "shop": "Apple产品京东自营旗舰店",
      "review_count": 1200000,
      "good_review_percent": 99,
      "rating": null,
      "stock_status": "in_stock"
    }
  ]
}
// Authentication required — context expired or no CN-IP available
{
  "success": false,
  "reason": "auth_required",
  "redirected_to": "https://passport.jd.com/new/login.aspx?ReturnUrl=...",
  "hint": "Refresh the Browserbase Context by QR-scanning login at JD app, or supply a China-resident external proxy."
}
// Captcha interposed mid-session
{
  "success": false,
  "reason": "captcha_required",
  "captcha_type": "slide_puzzle",
  "captcha_url": "https://iv.jd.com/...",
  "hint": "Use --solve-captchas or human-via-debug-url; current session has cookies that may still be valid after solve."
}

Marketplace status. This skill is shipped as a candidate — it documents the optimal honest path but cannot be validated end-to-end from Browserbase's default infrastructure (no China exit node, no shared JD credentials). Functional validation requires either (a) a tenant-supplied authenticated JD context, or (b) a tenant-supplied CN-residential proxy. Iteration log: 1 iter, 2026-05-20, converged on documenting the auth wall after exhausting search/list/item/wq/m-mobile/hk/global/api.m subdomains and Browserbase's CN-geolocation-hinted proxy (all confirmed-blocked from foreign IPs).