# AliExpress Deal Scanner — Discounts, Ratings & Sold Counts (`yzy001/aliexpress-deal-scanner`) Actor

Scrape AliExpress search results: title, product ID, sale and original price, discount %, currency, free-shipping and coupon tags, image and product URL. 60 products per request, any keyword, any shipping country and currency.

- **URL**: https://apify.com/yzy001/aliexpress-deal-scanner.md
- **Developed by:** [宗諭 楊](https://apify.com/yzy001) (community)
- **Categories:** E-commerce, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## AliExpress Search Scraper — Prices, Discounts & Free Shipping

### What sells, not just what costs

Every row now carries the two fields that tell you whether a listing matters:

- `starRating` — the listing's rating, or `null` where AliExpress does not show
  one. Measured: 46–56 of every 60 results have it.
- `soldText` — the sold count **exactly as the page shows it**, e.g. `5,000+ sold`.
- `soldAtLeast` — that string parsed to a number, and named for what it is: a
  **lower bound**. `5,000+` could be 5,000 or 9,999; only `soldText` preserves
  the bucket edge, so a gate refuses to emit the number without the string.

Those counts are localised, and the separators disagree. Measured across six
locales:

| Locale | As shown | Parsed |
|---|---|---|
| en_US | `5,000+ sold` | 5000 |
| de_DE | `1.000+ verkauft` | 1000 |
| ja_JP | `10,000+ 点販売` | 10000 |
| pt_BR | `50.000+  vendido(s)` | 50000 |

German and Brazilian listings use a **period** as the thousands separator, so
stripping commas alone turns `1.000+` into 1.0 — a thousandfold error, silently.
All eleven measured strings are in the test suite.

Missing values are `null`, never `0`. A zero would read as "measured, and it is
zero sales", which is a different claim from "the page did not say".

### When the source says no

If AliExpress returns its bot-mitigation page instead of results, this stops and
says so — it does not solve the challenge, rotate user agents, or retry around
it. The run fails with a message naming the fingerprint, so you know to slow
down or use a proxy rather than waiting for a parser fix.

That distinction matters: the same symptom (no products in a 200 response) can
mean the page changed or that the source is throttling you, and those need
opposite responses. They are reported as different errors.

Scrape AliExpress search results at **60 products per request**. Give it keywords
(or paste AliExpress search URLs) and get back a clean, flat dataset: title, product ID,
sale price, original price, discount %, currency, coupon and delivery tags, image and
product URL.

Prices on AliExpress change with the **destination country and currency**, so both are
inputs — set `Ship to` and `Currency` and you get the prices a buyer in that country
actually sees.

### What you get per product

| Field | Example |
|---|---|
| `title` | `Lenovo XT53 Wireless Bluetooth 5.3 Earphones Gaming Low Latency` |
| `productId` | `3256812453673234` |
| `url` | product page URL |
| `image` | image URL |
| `salePrice` | `11.33` |
| `originalPrice` | `31.22` |
| `discountPercent` | `63` |
| `currency` / `currencySymbol` | `USD` / `$` |
| `formattedPrice` | `US $11.33` |
| `hasPrice` | `true` |
| `skuId` | `12000060768086864` |
| `tags` | `["Save $19.89", "Delivery: Oct 16 - 22", "Free shipping"]` |
| `freeShipping` | `false` |
| `listedAt` | `2026-09-25 00:00:00` |
| `searchTerm`, `page`, `sourceUrl` | which query and page it came from |

Two ready-made dataset views: **Products** (everything, with thumbnails) and
**Biggest discounts** (sorted by discount %, for deal hunting and repricing).

### Filters

These are AliExpress's own search switches, not client-side filtering, so they change
what the site returns:

- **Free shipping**
- **On sale**
- **Ships from my country** (the site's "Local+")
- **AliExpress Choice**
- **Rating 4★ and up**
- **Ships from country** — a specific origin, e.g. `US`, `ES`, `CN`
- **Min / max price** in your chosen currency
- **Sort by** — best match, most orders, newest, price ascending or descending

Filters combine with AND. A narrow combination can legitimately return zero products;
the run finishes successfully and says so in the log and in `SUMMARY`, rather than failing.

### Typical uses

- **Price monitoring and repricing** — track a keyword or a competitor's catalogue over
  time; `salePrice`, `originalPrice` and `discountPercent` are separate fields, so a
  genuine price cut is distinguishable from a changed "original price".
- **Product and niche research** — `listedAt` shows when a product was listed, so you can
  separate new listings from long-standing ones.
- **Deal and coupon feeds** — the `tags` field carries the on-card offers
  (`Save $19.89`, `$1.04 each, ≥ 2 pieces`, `Lowest price in 90 days`, `Free shipping`).
- **Dropshipping sourcing** — filter to AliExpress Choice or a specific ship-from country
  to find stock that reaches your buyers quickly.

### Input example

```json
{
  "searchKeywords": ["wireless earbuds", "mechanical keyboard"],
  "maxItemsPerKeyword": 120,
  "currency": "USD",
  "shipTo": "US",
  "sortBy": "orders",
  "filters": ["freeShipping", "rating4Plus"],
  "minPrice": 5,
  "maxPrice": 50,
  "delaySeconds": 2
}
```

60 products come back per request, so `maxItemsPerKeyword: 120` is 2 requests per keyword.

### How it handles things going wrong

Most scrapers fail quietly: they return an empty dataset, or half a dataset, and the run
is marked successful. This one stops and tells you which check failed.

| Check | What it catches |
|---|---|
| Bot-detection page | The response is a challenge page rather than data. The run stops and reports it; **this Actor does not attempt to bypass bot detection.** |
| HTTP 200 with zero products | A shell page or a soft block. Not reported as "no products found". |
| Zero results, structure intact | Distinguished from the above: a filter combination with no matches finishes successfully with a note. |
| Page structure changed | The product array has moved. A structural fallback runs, and `SUMMARY.dataPathUsed` records that the fallback was used, so a silent change leaves a trace. |
| Pagination ignored | Page *N* returning page 1's products again. Overlap above 90% stops the run (a normal overlap is around 14%, from AliExpress repeating sponsored placements). |
| Wrong currency | Asking for USD and receiving another currency. Every field would look correct and every price would be wrong, so the run stops. |
| Price coverage collapse | Fewer than half the products parsing a price, which means the price field has moved. Records would still have titles and links and look fine. |
| Unknown filter value | An unrecognised filter is rejected instead of ignored, because ignoring it returns results you did not ask for. |
| Proxy requested but unavailable | Falling back to a direct connection would silently return another country's prices, so the run stops. |

`SUMMARY` (in the key-value store) records what actually ran: request count, record count,
duplicates skipped, records without a price, the filters applied, the data path used, and
a `caveats` list.

### Notes on the data

- Prices and shipping offers depend on the destination country; results for one `Ship to`
  do not transfer to another.
- AliExpress repeats sponsored placements across pages. Duplicates are removed by product
  ID and counted in `SUMMARY.duplicatesSkipped`.
- Search ordering is AliExpress's own and includes sponsored positions, so it is not a
  pure sales or relevance ranking.
- A small share of product cards carry no price block (0–15% in testing). Those records
  have `hasPrice: false` and are counted in `SUMMARY.recordsWithoutPrice`.
- Ratings and order counts are not present on the search cards and are therefore not in
  the output.

# Actor input Schema

## `searchKeywords` (type: `array`):

One or more keywords to search. Each keyword is scraped independently.

## `startUrls` (type: `array`):

Paste AliExpress search URLs directly, e.g. a filtered category page. The scraper appends page numbers itself.

## `maxItemsPerKeyword` (type: `integer`):

60 products per request, so 120 = 2 requests per keyword.

## `currency` (type: `string`):

Prices are returned in this currency.

## `shipTo` (type: `string`):

Prices, discounts and free-shipping offers differ per destination. Two-letter country code.

## `sortBy` (type: `string`):

How AliExpress orders the results. 'Most orders' is the usual choice for product research.

## `filters` (type: `array`):

AliExpress's own search switches. Verified 2026-10-10 that each one changes the result set.

## `shipsFrom` (type: `string`):

Only products shipping from this country, e.g. US, ES, CN. Two-letter code. Leave empty for any.

## `minPrice` (type: `integer`):

In the selected currency. Leave empty for no minimum.

## `maxPrice` (type: `integer`):

In the selected currency. Leave empty for no maximum.

## `locale` (type: `string`):

Site language, e.g. en_US, de_DE, es_ES, fr_FR. Affects product titles.

## `delaySeconds` (type: `integer`):

Be polite to the source. Lower values are faster but more likely to be rate-limited.

## `proxyConfiguration` (type: `object`):

Proxies are used for geo-targeting (prices differ by country) and to spread request rate. Recommended: residential, country matching 'Ship to'.

## Actor input object example

```json
{
  "searchKeywords": [
    "wireless earbuds",
    "mechanical keyboard"
  ],
  "maxItemsPerKeyword": 120,
  "currency": "USD",
  "shipTo": "US",
  "sortBy": "orders",
  "filters": [],
  "locale": "en_US",
  "delaySeconds": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `products` (type: `string`):

One row per product: title, prices, discount, currency, tags, image and URL.

## `summary` (type: `string`):

Request count, records, duplicates skipped, records without a price, filters applied, the data path used, warnings and caveats.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchKeywords": [
        "wireless earbuds",
        "mechanical keyboard"
    ],
    "maxItemsPerKeyword": 120,
    "sortBy": "orders"
};

// Run the Actor and wait for it to finish
const run = await client.actor("yzy001/aliexpress-deal-scanner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchKeywords": [
        "wireless earbuds",
        "mechanical keyboard",
    ],
    "maxItemsPerKeyword": 120,
    "sortBy": "orders",
}

# Run the Actor and wait for it to finish
run = client.actor("yzy001/aliexpress-deal-scanner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchKeywords": [
    "wireless earbuds",
    "mechanical keyboard"
  ],
  "maxItemsPerKeyword": 120,
  "sortBy": "orders"
}' |
apify call yzy001/aliexpress-deal-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yzy001/aliexpress-deal-scanner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/7Y9zca86X5ntWHeop/builds/Ib1BW2UtzXwdeDSuw/openapi.json
