Scrape AliExpress product data by search keyword, product URL, or product ID -- price, rating, order count, per-variant stock levels, and shipping-cost estimates to your destination country, delivered as clean typed rows for dropshippers and product-sourcing research. A hosted AliExpress API.
Un-shelve verification pass. Build 0.1.2 (cloud QA run YSBcad4xebfIygDll,
5 rows) was produced from a commit that had never been fast-forwarded onto
main -- this release lands that source and adds one hardening change on
top of it:
ResultRow delivery assertion: product_id must be numeric (8-16
digits), price must be > 0 (was >= 0), title must be at least 3
chars, and product_url must embed the row's own product_id. This is
the guard the ziprecruiter-jobs-scraper fabrication incident
(2026-08-30 -- a SUCCEEDED run with 3 rows built from nav links and the
search page's own URL) proved this fleet needs on every Actor: a
blocked-but-200 page can no longer produce a row that passes.
Independently verified 3/5 dataset rows from YSBcad4xebfIygDll against
live AliExpress product pages (og:title + og:image byte-for-byte match);
all three are real, currently-listed products.
0.1.0 -- 2026-08-31
First genuinely working release. The scaffold shipped 2026-08-02 was never
functional (window.runParams was the wrong variable on both surfaces --
see notes.md for the full recon trail) and every previous cloud QA attempt
returned 0 real product rows.
Rewrote parser.py against the wire format confirmed live 2026-08-31
(local curl-cffi + Apify RESIDENTIAL(US) probe for search, cloud Camoufox
render for detail):
Search results live in window._dida_config_._init_data_, not
window.runParams -- a plain curl-cffi fetch over RESIDENTIAL gets the
full result list (title/price/currency/rating/order-count-hint/images)
with no browser needed.
Detail pages are architecturally client-side-rendered (confirmed with
zero exceptions across every product ID tried, any proxy tier); real
data only appears in an application/ld+json schema.org Product block
after a Camoufox render, never in a window variable.
search_keywords rows no longer need a detail-page fetch at all --
built directly from the search page's own fields. This mode stays on the
cheap curl-cffi path end to end.
product_urls/product_ids rows now go through a real Camoufox
render (new src/detail_browser.py) since the CSR shell never carries
data over plain HTTP. One browser per run, opened lazily only when these
modes are used, so search-only runs never pay for it.
Proxy now defaults to RESIDENTIAL pinned to shippingDestinationCountry
(previously unset) -- AliExpress returns a plausible-looking wrong-locale
page from a mismatched exit (HTTP 200, not an error), and datacenter IPs
hit a JS-redirect challenge on the search surface. Added a currency-
mismatch guard that reports geo-mismatch instead of ingesting corrupted
rows.
order_count/seller_name/category/variants are honestly null/[]
for product_urls/product_ids rows -- no live-confirmed source carries
them yet (the ld+json block doesn't). shipping_cost_estimate stays
null for every row -- the guessed freight endpoint 404s (confirmed
again 2026-08-31); a real endpoint was never found.
Regenerated uv.lock -- camoufox/playwright were added to
pyproject.toml on 2026-08-26 but the lock was never refreshed, so every
Camoufox-dependent build silently relied on stale Docker layer cache.
Corrected .actor/pay_per_event.json's actor-start price to the fleet
standard $0.05 flat (was $0.005).
Fixed a REQ-10 zero-rows bug found in the pre-publish gate check:
_finish() used to fail the whole run whenever total rows was zero,
including a keyword that legitimately matched nothing on AliExpress --
the exact anti-pattern that got reverb-sold-listings delisted
(DEVILSCRAPES-CONTEXT.md "an empty result is not a failure"). Now only
fails loud when every single input is genuinely unresolved (fetch/render
failure, geo-pin mismatch, bad URL) -- a "no-results" search SUCCEEDS
with zero rows and an honest status message.
Typed main.py's internal session/detail-browser parameters against
narrow Protocols (SearchPageFetcher/ShippingCostFetcher/
SearchSession/DetailFetcher) instead of the concrete
AliExpressSession/DetailBrowser classes, fixing 11 pyright errors
where tests/test_main.py's plain stub doubles weren't structurally
assignable to the concrete types.
Added tests/test_smoke.py (-m smoke, opt-in, skips-not-fails on
network/anti-bot noise) covering both live fetch paths -- the publish
gate requires a smoke layer and none existed for this Actor.