# LEGO Sale & Retiring Sets Monitor: Price Drop Alerts (`accountable_eel/lego-sale-retiring-lookup`) Actor

LEGO.com sale and retiring-soon sets to Google Sheets or Discord: price, list price, % off, stock and the 'retiring soon' flag per set. Get alerts for new sales, price drops, sets just flagged retiring and back in stock. US, UK, Canada, Germany. No login, no images.

- **URL**: https://apify.com/accountable_eel/lego-sale-retiring-lookup.md
- **Developed by:** [Adriana Voss](https://apify.com/accountable_eel) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 set row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## LEGO Sale & Retiring Sets Monitor: Price Drop Alerts

Read the **LEGO.com sale page** and the sets LEGO marks **"Retiring soon"** as one spreadsheet-ready row per set: price, the price before the sale, percent off, stock status and the retiring flag. Switch on monitoring and the actor remembers what each set cost last time, then tells you which sets are **newly on sale**, **dropped in price**, **just got flagged as retiring**, **came back in stock** or **disappeared**, and posts that to Discord, Slack or Telegram.

It reads LEGO's own list pages, the ones you see in a browser, for the United States, United Kingdom, Canada and Germany. No login, no API key. It returns prices and stock facts only: no images, no descriptions.

### Who uses this

- **Resellers and flippers** who buy LEGO on sale or just before retirement and sell it on BrickLink, eBay or Vinted. Run it every few hours and get a message when a set you care about drops or gets flagged retiring.
- **Deal channels** (Discord, Telegram, Slack) that post LEGO deals. One run per storefront, an alert only when something moved.
- **Collectors and spreadsheet people** who want the whole sale list, or one theme such as Star Wars, as a table with the discount worked out.
- **n8n, Make and Clay builders** who need LEGO sale data inside a workflow.

### Try it in 30 seconds

Press **Start** with the pre-filled input. It reads the US LEGO Sale page and returns the first 21 sets:

```json
{ "lists": ["sale"] }
```

Add the retiring list and keep only big discounts on Star Wars sets:

```json
{ "lists": ["sale", "retiring-soon"], "themes": ["Star Wars"], "minDiscountPct": 20 }
```

### What you get

One row per set. A set that is on both the sale and the retiring list is returned (and billed) once, with `listsFound` showing both.

```json
{
  "setNumber": "76470", "name": "Enchanted Flying Ford Anglia™", "theme": "Harry Potter™",
  "locale": "en-us", "currency": "USD", "price": 63.99, "listPrice": 79.99, "discountPct": 20,
  "retiringSoon": true, "availability": "E_AVAILABLE", "canAddToBag": true,
  "pieces": 868, "ageRange": "14+", "list": "sale", "listsFound": ["sale"]
}
```

The lists also carry accessories and apparel (a T-shirt, a hat), which have longer product codes. Filter on `setNumber` or `theme` in your sheet if you only want sets. A product with several variants (sizes) is one row at its cheapest variant, and `variantCount` says how many.

### Monitoring: a price-drop alert in three steps

1. **Turn on monitoring.** Tick **Track changes between runs**. Sets with no change you asked about are left out and not billed, so a quiet run returns one summary row (`setCount` 0) and costs only the start fee. Tick **Remember silently on the first run** if you do not want the whole sale page returned (and posted) on run one.
2. **Pick what to hear about.** **Alert on** takes one word per line: `new-sale` (on sale and not before), `price-drop` (at least **Smallest price drop**, 5% by default), `now-retiring` (LEGO just flagged it), `back-in-stock`, and `gone` (the set left the list it was on; only trusted when the whole list was read). Each row's `changeType` says which fired, with `previousPrice`, `changePct` and `lowestEverSeen`.
3. **Send it somewhere.** Paste a Discord or Slack webhook, a Telegram bot link or any URL into **Send alerts to**. Each alert arrives as one message with the name, set number, price and what it was, theme, pieces and link. For n8n or Make leave the format on JSON, or skip the link and read the dataset for rows where `changeType` is `drop`, `new`, `now-retiring`, `back-in-stock` or `gone`.

Schedule the saved input every few hours. Alerts are sent only for rows that were billed, messages are never billed, and a failed message never fails the run (the rows' `notifyErrors` says why).

### Example runs

**Resellers who want to know the moment a set gets flagged retiring.** Change `themes`.

```json
{"lists": ["retiring-soon"], "themes": ["Star Wars", "Icons"], "deltaMode": true, "deltaName": "retiring-watch", "alertOn": ["now-retiring", "price-drop"], "notifyUrl": "<your Discord webhook>"}
```

**A daily Discord post of new deep discounts.** Change `minDiscountPct`.

```json
{"lists": ["sale"], "minDiscountPct": 30, "deltaMode": true, "deltaName": "deep-sales", "skipFirstRun": true, "alertOn": ["new-sale"], "notifyUrl": "<your Discord webhook>"}
```

**The whole UK sale list in a sheet, no monitoring.** Change `locale`.

```json
{"lists": ["sale"], "locale": "en-gb", "maxItems": 100}
```

### Limits, stated plainly

- **It returns LEGO.com's list pages as shown.** LEGO.com decides what is on sale, what is flagged retiring and the order (its Recommended order). The actor adds no sort of its own, and the server-side sort option of the page does not deliver a list.
- **Four storefronts.** United States, United Kingdom, Canada and Germany. Sale works on all four, retiring soon on en-us, en-gb and de-de, new on en-us and de-de. A list a storefront does not serve is skipped, with a note in the log. France returned an empty sale page when it was checked.
- **A retiring flag is LEGO's flag, not a retirement date.** There is no date on the page. Sets flagged "retiring soon" are often at full price: use `discountPct` and `minDiscountPct` to separate sale from retiring.
- **No minifigure counts.** LEGO.com's list pages carry no minifigure count, so the column does not exist. Piece count is there when LEGO states one.
- **Prices only: no images, no descriptions, no checkout.** The actor never opens a product page, the bag or an account page. It reads only the `/categories/` list pages that LEGO.com's robots.txt allows, one page per second.
- **No secondary-market prices.** LEGO.com has no sold price. BrickLink and eBay comparisons are not included.
- **`gone` needs a full read.** A set is called gone only when every list it was last seen on was read to its last page with every tile parsed. A run that stops early (your `maxItems`, a refused page, the time budget) never calls anything gone.
- **At most 300 sets per list.** The run stops at the last page, at 20 pages per list, or after about three minutes.
- **Misses are free.** Blocked, a changed page, an empty list: nothing is charged.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `lists` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~lego-sale-retiring-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"lists":["sale"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "lists": [
    "sale"
  ]
}
```

One list per line: sale, retiring-soon or new. Leave it as sale for the LEGO Sale page, add retiring-soon for the sets LEGO marks as retiring soon (the page LEGO calls Last chance to buy). A pasted LEGO.com category link is read for its list name. Prices and stock facts only: no images, no descriptions, no login. Accepted formats: sale, retiring-soon, new, https://www.lego.com/en-us/categories/sales-and-deals.

### Sample output

| query | found | status | setCount | setsRead | listsChecked | listsFailed | truncated | monitorStatus | sets | setNumber | name | theme | url | locale | currency | price | listPrice | discountPct | retiringSoon | featuredFlags | availability | canAddToBag | pieces | rating | ageRange | list | listsFound | checkedAt | changeType | changes | previousPrice | changePct | lowestEverSeen | firstSeenAt | notifyErrors | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| sale | true | OK | <sets returned> | <sets read> | <lists checked> | <lists that stopped early> | <stopped short> | <monitoring status> | \<all sets (full list)> | <set number> | <name> | <theme> | <product page> | <storefront> | <currency> | <price> | <list price> | \<discount %> | <retiring soon> | \<lego.com flags> | <availability> | <can add to bag> | <pieces> | <rating> | <age range> | <list> | <all lists it is on> | <checked at> | <change since last run> | <all changes detected> | <price last run> | \<price change %> | <lowest price seen> | <first seen> | <alert delivery errors> | 1970-01-01T00:00:00.000Z |

A miss comes back as a row with `"found": false` and is never charged.

### Pricing

Pay-per-event. A flat per-run fee covers session/proxy warmup; you're billed
per item only when data is actually found and returned — see
`.actor/pay_per_event.json` for exact prices. A miss is never charged.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~lego-sale-retiring-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"lists":["sale"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~lego-sale-retiring-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"lists":["sale"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~lego-sale-retiring-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"lists":["{{LEGO.com list}}"]}`, mapping the row's LEGO.com list into the `lists` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "LEGO Sale & Retiring Sets Monitor | Apify" — the agent will find and run this actor.

### FAQ

**Is this the official LEGO API?** No. It reads the public LEGO.com list pages without logging in, only the `/categories/` paths that robots.txt allows, and takes the embedded product data of each page.

**Which sets does "retiring soon" show?** The ones LEGO.com tags "Retiring soon" on its Last chance to buy page. The tag is LEGO's; there is no retirement date on the page.

**Why is a set missing that I can see on LEGO.com?** The lists are read as the server sends them. A tile that is not a product (a gift-with-purchase banner) is skipped, and `maxItems`, `themes`, `minDiscountPct` and `retiringOnly` all narrow the result. `setsRead` shows how many different sets the run saw before those filters.

**What does a watchlist remember?** For each set: last price, lowest price seen, whether it was on sale, flagged retiring and in stock, and which lists it was on. It is stored in an Apify key-value store under your account and capped at 5,000 sets.

**Does it work with a datacenter proxy?** The default proxy is Apify's datacenter pool. If LEGO.com refuses it you get a free BLOCKED row; switch the proxy to Residential in the input.

# Actor input Schema

## `lists` (type: `array`):

One list per line: sale, retiring-soon or new. Leave it as sale for the LEGO Sale page, add retiring-soon for the sets LEGO marks as retiring soon (the page LEGO calls Last chance to buy). A pasted LEGO.com category link is read for its list name. Prices and stock facts only: no images, no descriptions, no login. Accepted formats: sale, retiring-soon, new, https://www.lego.com/en-us/categories/sales-and-deals. You're only charged for the ones we actually find — a miss costs nothing.

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

Which LEGO.com storefront (and currency) to read. Sale: all four. Retiring soon: en-us, en-gb, de-de. New: en-us, de-de. A list a storefront does not serve is skipped with a note in the log.

## `maxItems` (type: `integer`):

Up to 300. Counted after your filters, per list, and it is your budget control: you pay per set returned. LEGO.com shows 19 to 24 sets per page, and the actor reads another page only while it needs more.

## `minDiscountPct` (type: `integer`):

Optional, 0 to 95. Applied here after the page is read (LEGO.com's page cannot filter by discount for us). 0 keeps every set, including full-price sets on the retiring list.

## `themes` (type: `array`):

Optional. One theme per line, e.g. Star Wars or Harry Potter. Case and the trademark sign do not matter; a line matches any theme that contains it. Applied here after the page is read.

## `retiringOnly` (type: `boolean`):

Keeps only sets LEGO.com flags 'Retiring soon'. Applied here after the page is read.

## `deltaMode` (type: `boolean`):

Turns this into a watchlist. Each run compares every set with what the same watchlist saw last time and fills in 'Change since last run', 'Price last run', 'Price change %' and 'Lowest price seen'. By default only sets that changed in a way you asked for below come back, so a quiet run returns one summary row and bills nothing. Schedule it every few hours and send the alerts to Discord, Slack or Telegram with 'Send alerts to' below. The first run lists everything as new.

## `alertOn` (type: `array`):

One per line. new-sale (on sale and not before), price-drop, now-retiring (LEGO just flagged it), back-in-stock, gone (no longer on the list it was on; only trusted when the whole list was read). Leave the default for the first four.

## `minPriceDropPct` (type: `integer`):

A set must be at least this much cheaper than when the watchlist last saw it to count as a price drop.

## `alertOnChangeOnly` (type: `boolean`):

On by default whenever monitoring is on: sets with no change you asked about are left out and not billed. Turn it off to get every set back each run, annotated with its change columns (and billed).

## `skipFirstRun` (type: `boolean`):

The first run of a watchlist has nothing to compare with. Turn this on to remember every set without returning or billing it, so an alert link does not post the whole sale page.

## `deltaName` (type: `string`):

Leave empty and a name is derived from your lists, storefront and filters, so two schedules with different filters keep separate memories. Type your own name to keep one memory across changes.

## `notifyUrl` (type: `string`):

Optional. After the sets are billed, each alert set is posted here as one message: name, set number, price (and what it was), theme, pieces and link, with a NEW ON SALE / PRICE DROP -X% / NOW RETIRING SOON / BACK IN STOCK / GONE badge. No images. Discord or Slack: paste the channel's webhook URL. Telegram: https://api.telegram.org/bot<token>/sendMessage?chat_id=<chat id>. Any other URL receives JSON. Messages are free, and a message that fails never fails the run (the rows' 'Alert delivery errors' says why). Stored encrypted, never logged or written to the results.

## `notifyFormat` (type: `string`):

Leave on auto and the format is detected from the link.

## `maxNotificationsPerRun` (type: `integer`):

Up to 100. Above this, one '+N more' message says how many more alerts are in the run's dataset, so a busy first run never floods the channel. 0 sends only that summary.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each set found gets its own row instead of being grouped under its LEGO.com list. You're still only charged once per LEGO.com list, no matter how many rows it produces.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "lists": [
    "sale"
  ],
  "locale": "en-us",
  "maxItems": 21,
  "minDiscountPct": 0,
  "themes": [],
  "retiringOnly": false,
  "deltaMode": false,
  "alertOn": [
    "new-sale",
    "price-drop",
    "now-retiring",
    "back-in-stock"
  ],
  "minPriceDropPct": 5,
  "alertOnChangeOnly": true,
  "skipFirstRun": false,
  "deltaName": "",
  "notifyFormat": "auto",
  "maxNotificationsPerRun": 20,
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "columns": [
    "setCount",
    "setsRead",
    "listsChecked",
    "listsFailed",
    "truncated",
    "monitorStatus",
    "sets",
    "setNumber",
    "name",
    "theme",
    "url",
    "locale",
    "currency",
    "price",
    "listPrice",
    "discountPct",
    "retiringSoon",
    "featuredFlags",
    "availability",
    "canAddToBag",
    "pieces",
    "rating",
    "ageRange",
    "list",
    "listsFound",
    "checkedAt",
    "changeType",
    "changes",
    "previousPrice",
    "changePct",
    "lowestEverSeen",
    "firstSeenAt",
    "notifyErrors"
  ],
  "expandRows": true,
  "maxConcurrency": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "lists": [
        "sale"
    ],
    "themes": [],
    "alertOn": [
        "new-sale",
        "price-drop",
        "now-retiring",
        "back-in-stock"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/lego-sale-retiring-lookup").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 = {
    "lists": ["sale"],
    "themes": [],
    "alertOn": [
        "new-sale",
        "price-drop",
        "now-retiring",
        "back-in-stock",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/lego-sale-retiring-lookup").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 '{
  "lists": [
    "sale"
  ],
  "themes": [],
  "alertOn": [
    "new-sale",
    "price-drop",
    "now-retiring",
    "back-in-stock"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/lego-sale-retiring-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/lego-sale-retiring-lookup"
        }
    }
}
```

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/xavPugqH0DGW2X5p1/builds/PMsnBAllzN9nWWRp1/openapi.json
