Purpose
Given a product name / search query (e.g. "wireless headphones", "iphone 15 case"), return a structured list of matching AliExpress listings — for each result: product id, canonical detail-page URL, title, current price, original/list price (when discounted), discount %, star rating, sold count, and promotional badges. Read-only — never adds to cart, never proceeds to checkout, never signs in.
When to Use
- Quick product discovery: "find me {query} on AliExpress".
- Comparing the cheapest or best-rated listings for a generic product across sellers.
- Building a price/availability watcher for a query slug.
- Feeding a downstream "fetch product detail" skill — this skill returns the
productIdand canonical detail URL each item-detail skill needs.
Workflow
The recommended path is browserless_agent, browser-driven. There is no public JSON API for the storefront search surface — items are server-side-rendered into the page's HTML, and the GraphQL/MTOP endpoints AliExpress uses internally (gdp.alicdn.com/mtop.aliexpress.*) require Alibaba-signed m-h5-tk + _m_h5_tk_enc browser cookies and a per-request sign derived from those (not reproducible from a cookieless client). The page-rendered HTML is the cheapest reliable surface. A residential proxy is required — pass proxy: { proxy: "residential" } on the call (repeat the same proxy on every call — that keeps you in the same session; dropping or changing it lands you in a different, blank session); bare/datacenter sessions trip Cloudflare/Akamai-style verification challenges.
Important architectural note for the agent reading this: search-results items are NOT in window._dida_config_._init_data_ (the page-state blob). The cards2023_* field where you'd expect them is empty — items are baked directly into the rendered DOM. Don't waste turns inspecting that object.
1–2. Navigate to the canonical search URL
Slugify the query (lowercase, ASCII letters/digits only, spaces → hyphens), then run one browserless_agent call: goto the canonical URL, wait ~3.5 s for the grid to hydrate, then evaluate the extractor (step 3):
{
"rationale": "Searching AliExpress products",
"proxy": { "proxy": "residential" },
"commands": [
{
"method": "goto",
"params": {
"url": "https://www.aliexpress.com/w/wholesale-wireless-headphones.html",
"waitUntil": "load",
"timeout": 45000,
},
},
{ "method": "waitForTimeout", "params": { "time": 3500 } },
{
"method": "evaluate",
"params": { "content": "<extractor IIFE, step 3>" },
},
],
}If the page shows a "Verify you're human" / captcha screen, try the solve command, then retry with a fresh call. Equivalent URL form: https://www.aliexpress.com/wholesale?SearchText=<urlenc> — AliExpress 302-redirects it to the canonical /w/wholesale-<slug>.html. Prefer the canonical form to skip the redirect.
Equivalent URL form: https://www.aliexpress.com/wholesale?SearchText=<urlenc> — AliExpress 302-redirects this to the canonical /w/wholesale-<slug>.html path. Prefer the canonical form to skip the redirect round-trip.
Optional URL params (appended as ?key=val&...):
| Param | Meaning |
|---|---|
SortType=default | Best Match (default) |
SortType=total_tranpro_desc | Sort by Orders (most-sold first) |
SortType=price_asc / price_desc | Sort by price ascending / descending |
SortType=latest_desc | Newest listings first |
minPrice=N&maxPrice=N | Numeric price bounds in the page's currency |
shipFromCountry=US,CN,... | Filter origin country (comma-separated ISO-2) |
g=y | Filter to "Choice" (AliExpress-curated faster shipping) items |
Unrecognized params are silently dropped.
3. Extract listings from the rendered DOM
Each search result is an <a href="/item/<productId>.html"> anchor (hostname is either www.aliexpress.com or www.aliexpress.us — both resolve to the same product). The anchor's innerText is already neatly line-broken by the page's CSS — split on \n, trim, and classify each line by pattern. The DOM uses obfuscated class names (k7_kg, k7_l7, nc_nf, …) that change per build — selecting by class is brittle; rely on the line-split heuristic below instead.
Pass this as the evaluate content (wrap the final return items as return JSON.stringify(items) so it comes back as a string under .value):
(() => {
const seen = new Set();
const items = [];
for (const a of document.querySelectorAll('a[href*="/item/"]')) {
const m = a.href.match(/\/item\/(\d+)\.html/);
if (!m) continue;
const id = m[1];
if (seen.has(id)) continue;
seen.add(id);
const lines = (a.innerText || "")
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (lines.length === 0) continue;
const item = {
productId: id,
url: a.href.split("?")[0], // strip tracking params
title: lines[0],
price: null,
listPrice: null,
discountPct: null,
rating: null,
sold: null,
badges: [],
};
const prices = [];
for (let i = 1; i < lines.length; i++) {
const ln = lines[i];
if (/^\$[\d,]+(\.\d{1,2})?$/.test(ln)) {
prices.push(ln);
continue;
}
if (/^-\d{1,2}%$/.test(ln)) {
item.discountPct = ln;
continue;
}
if (item.rating === null && /^[1-5]\.\d$/.test(ln)) {
item.rating = parseFloat(ln);
continue;
}
if (item.sold === null && /\bsold$/i.test(ln)) {
item.sold = ln;
continue;
}
item.badges.push(ln);
}
item.price = prices[0] || null;
item.listPrice = prices[1] || null; // present only when discounted
// Drop "related search keyword" anchors — they have no price/sold and are not real products.
if (!item.price && !item.sold) continue;
items.push(item);
}
return JSON.stringify(items); // string back via evaluate .value
})();Filter rule (critical): anchors with only a title line (no price, no sold count) are related-search keyword shortcuts AliExpress injects into the grid — they are NOT real products and clicking them runs another search. Drop them with the if (!item.price && !item.sold) continue; guard above.
4. (Optional) Load more results via infinite scroll
The grid is JS-paginated by scroll, not by ?page=N (anchor scans for a[href*="page="] return zero hits). Each scroll-to-bottom triggers an XHR that appends ~12–17 more items to the DOM. Batch scroll + wait commands in one browserless_agent call (keep proxy), then a final evaluate re-runs the extractor (it dedupes by productId via the seen Set):
"commands": [
{ "method": "scroll", "params": { "direction": "down" } },
{ "method": "waitForTimeout", "params": { "time": 2000 } },
{ "method": "scroll", "params": { "direction": "down" } },
{ "method": "waitForTimeout", "params": { "time": 2000 } },
{ "method": "evaluate", "params": { "content": "<extractor IIFE>" } }
]Observed: initial render ~17 items; +17 per scroll batch; pages cap around 50–60 items per query in normal use. Don't infinite-loop — set a hard cap. (scroll without a selector scrolls the page; repeat the command to append more batches.)
5. Session lifecycle
No release step — the session persists across calls, keyed by proxy. To scroll for more results you can keep the scroll + extract within one call's commands array (as above); that saves round-trips and keeps you on the hydrated page. A separate call carrying the same proxy reconnects to that same warmed page, but dropping or changing proxy lands you in a different, blank session back at the first page.
Site-Specific Gotchas
- A residential proxy is required. Pass
proxy: { proxy: "residential" }on everybrowserless_agentcall; bare/datacenter sessions intermittently hit Cloudflare/Akamai verification screens. If a captcha appears,solvecan attempt it before you retry with a fresh call. - Items are NOT in
window._dida_config_._init_data_. The page-state object exposeshierarchy, decode tables, and refine filters, butdata.data.cards2023_*(where items would logically live) is empty{}— the rendered grid is the canonical source. Don't waste turns spelunking_dida_config_for products. - No JSON storefront API for search. AliExpress's internal MTOP/GraphQL surface (
gdp.alicdn.com/mtop.aliexpress.search.*) requires per-request Alibabasignheaders derived from session cookies (_m_h5_tk,_m_h5_tk_enc). These can't be replayed from a cookielesscurland reverse-engineering the sign function is out of scope. The browser-rendered HTML is the practical surface. - Class names are obfuscated and per-build (
k7_kg,k7_l7,nc_nf, …). Don't select by class — they change. Use theinnerTextline-split heuristic ona[href*="/item/"]anchors. - No-results queries silently fall back to "anything goes". A query like
zxqzxqzxq123nonsensereturns ~5–6 unrelated products with no "0 results" banner. To detect a truly poor match, compare the query tokens against returned titles — if median title-token overlap is < 1, treat it asno_match. - Related-search anchors masquerade as products. Inside the grid, AliExpress injects keyword-shortcut anchors (e.g.
<a href="/item/.../1005002856476808.html">iphone 15 vans case</a>) that have only a title and no price/rating/sold. Filter them out by requiringprice || soldto be present. - Two product-page hostnames coexist:
www.aliexpress.comandwww.aliexpress.us. Same productId, same content, different geo/locale routing. Treat either as canonical; don't normalize one to the other without a reason. - The URL pattern is canonicalised to a hyphenated slug.
/wholesale?SearchText=wireless+headphones302s to/w/wholesale-wireless-headphones.html. Special characters (',&, accents) are dropped from the slug —iPhone 15 Pro Maxbecomesiphone-15-pro-max. Use?SearchText=…if you need to preserve weird query strings, accept the redirect. - Anchor URLs include heavy tracking params (
algo_pvid,pdp_npi,algo_exp_id,curPageLogUid,utparam-url). Always strip with.split('?')[0]for a clean canonical detail URL. The/item/<id>.htmlpath alone is sufficient to fetch the product. - Rating may be missing on cards. New listings (or those AliExpress chooses to suppress) omit the
^[1-5]\.\d$rating line entirely; the sold line may also be absent for brand-new items. Code defensively for nulls in both. - Pagination is infinite-scroll only — no
?page=N. Sorting viaSortType=and filtering viaminPrice/maxPrice/shipFromCountrywork via URL params, but page-number navigation does not exist in the URL contract. - Wait
~3.5safterwait loadbefore extracting. The grid hydrates progressively post-load; extracting immediately may miss the first batch of items. - The
BundleDealsURL is not a product page. A handful of anchors point to/ssr/<id>/BundleDeals2?productIds=...(a multi-product bundle landing page). The current extractor filters them naturally (no/item/<id>.htmlmatch), but be aware they appear in the grid.
Expected Output
{
"query": "wireless headphones",
"search_url": "https://www.aliexpress.com/w/wholesale-wireless-headphones.html",
"sort": "default",
"result_count": 18,
"items": [
{
"productId": "3256811752642309",
"url": "https://www.aliexpress.us/item/3256811752642309.html",
"title": "B36 Wireless Bluetooth 5.3 Over-Ear Headphones with ANC Noise Cancelling 8H Playtime Ergonomic Design HD Microphone Foldable",
"price": "$8.64",
"listPrice": null,
"discountPct": null,
"rating": 4.9,
"sold": "179 sold",
"badges": ["$2 off on $18", "Save $14.45"]
},
{
"productId": "3256806808326673",
"url": "https://www.aliexpress.us/item/3256806808326673.html",
"title": "Transparent Magnetic Case For iPhone 15 14 13 Pro Max For Magsafe Clear Wireless Charging Phone Cases",
"price": "$2.75",
"listPrice": "$6.91",
"discountPct": "-60%",
"rating": 4.9,
"sold": "100K+ sold",
"badges": ["$2 off on $18", "New shoppers save $4.16"]
}
]
}Distinct outcome shapes:
// 1. Normal results (above)
{ "result_count": N, "items": [...] }
// 2. Nonsensical query — AliExpress returns unrelated fallback items, NOT a 0-results page.
// Detect via low query-token overlap with returned titles.
{
"query": "zxqzxqzxq123nonsense",
"result_count": 6,
"match_quality": "no_match",
"items": [ /* unrelated products — surface them as low-confidence or drop */ ]
}
// 3. Verification challenge (rare, on degraded sessions). Detect via page title
// "Verify yourself" / "Are you human" / Cloudflare branding. Recreate session.
{
"query": "...",
"error": "verification_challenge",
"hint": "Retry the browserless_agent call with proxy:{proxy:'residential'} set; try the solve command, else a fresh call."
}