# TCG Price Movers & Spike Alerts - Pokemon, Magic, Cardmarket (`lowlanddata/tcg-price-movers`) Actor

Trading cards actually moving on Cardmarket, or an alert when one on your watchlist moves. Pokemon, Magic, Yu-Gi-Oh and 16 more games, read from Cardmarket's own daily price files.

- **URL**: https://apify.com/lowlanddata/tcg-price-movers.md
- **Developed by:** [Lowland Data](https://apify.com/lowlanddata) (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 $18.50 / 1,000 per alert delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TCG Price Movers & Spike Alerts - Pokemon, Magic, Cardmarket

Finds the trading cards whose price is actually moving on **Cardmarket**, Europe's biggest card marketplace - and, if you want, alerts you when a card on your own watchlist moves. Pokémon, Magic, Yu-Gi-Oh!, Lorcana, One Piece and sixteen more games. One row per card: what it costs today, what it moved from, the percentage, and the daily, weekly and monthly averages behind it.

**Live health record:** [lowlanddata.com/status/tcg-price-movers](https://lowlanddata.com/status/tcg-price-movers/) - 30-day success rate and daily canary results, failures included. Full input/output reference: [tool page](https://lowlanddata.com/tools/tcg-price-movers/).

No scraping is involved: it reads the price-guide files Cardmarket itself publishes every day. That is why a run finishes in under a minute across a whole game, never breaks on a site redesign, and never touches a seller - there is no personal data in the source or in the output.

### Quick start (30 seconds, no keys needed)

1. Hit **Start** with the prefilled settings: Pokémon, gainers, cards over €2, moves of 15% or more. You get today's biggest risers, largest first.
2. Change the **game**, the **direction** (gainers, losers or both), or add **card names** to narrow it to what you collect.
3. To be told rather than to look: set **mode** to `alerts`, add a daily schedule, and you get a row only when one of your cards actually moves.

### Who uses this

- **Collectors and investors** watch a set of names (`cardNames: ["charizard", "umbreon"]`) and get told when one spikes, instead of checking pages by hand.
- **Sellers** watch the losers list for the game they stock, because a card sliding 30% is inventory to move before it slides further.
- **Traders** run gainers on Pokémon every morning and read the top twenty as a market briefing.
- **Newsletter and content people** take the weekly movers as ready-made material - the rows are already ranked and sourced.
- **Shops pricing a bulk buy-in** run `direction: both` over an expansion to see which cards in a collection are drifting.

### What you get

One JSON item per card variant that moved. A riser:

```json
{
  "game": "Pokémon",
  "cardName": "Ash's Pikachu [Iron Tail | Thunderbolt]",
  "idProduct": 778123,
  "expansionId": 5432,
  "variant": "regular",
  "priceEur": 207.65,
  "movedFromEur": 46.47,
  "movedToEur": 162.56,
  "changePct": 249.8,
  "direction": "up",
  "comparisonWindow": "7-day vs 30-day average",
  "vs30DayPct": 346.8,
  "trendEur": 207.65,
  "lowEur": 149.9,
  "avg1DayEur": 198.0,
  "avg7DayEur": 162.56,
  "avg30DayEur": 46.47,
  "currency": "EUR",
  "priceGuideDate": "2026-09-07",
  "productUrl": "https://www.cardmarket.com/en/Pokemon/Products?idProduct=778123"
}
```

Read that row as a ladder: the card averaged €46 over the month, €163 over the past week, and trades at €208 today - and the cheapest copy on sale is already €149.90. Everything agrees, which is what makes it a real move rather than a blip.

#### What `priceEur` is, and what you will see when you click through

Being straight about this, because it surprises people: **the number on the Cardmarket page will usually not be the number in your row, and neither is wrong.**

`productUrl` opens the card's own page, which leads with **From** - the cheapest copy on sale right now. This actor leads with `priceEur`, Cardmarket's **trend price**: what the card has actually been selling for. They drift apart in both directions, often by a lot:

| Card (2026-09-09 guide)   | `priceEur` (trend) | Cheapest offer on the page |
| ------------------------- | ------------------ | -------------------------- |
| Sobble (S-P 146)          | €53.84             | €29.88                     |
| Celebratory Fanfare (254) | €230.12            | €400.00                    |

If you want the number the page shows, use **`lowEur`** - that is the same figure as **From**, to the cent. `priceEur` answers "what does this card go for", `lowEur` answers "what would one cost me today".

The offers on that page also mix conditions, languages and graded slabs - one ordinary card runs from a €29.88 near-mint raw copy to a €999 PSA 10 - and Cardmarket's averages are computed across that mix. That is the same blending that makes the [corroboration test](#the-honest-part-why-most-movers-lists-are-noise) necessary, and it is why a single price for a card is a blend rather than a quote for one specific copy.

A faller looks the same with the ladder inverted:

```json
{
  "cardName": "Charizard [Energy Burn | Fire Spin]",
  "variant": "regular",
  "priceEur": 57.27,
  "avg30DayEur": 344.39,
  "changePct": -83.4,
  "direction": "down",
  "vs30DayPct": -83.4,
  "lowEur": 54.0
}
```

And in `alerts` mode, a row arrives only when a card you are watching has moved since the monitor last looked:

```json
{
  "cardName": "Umbreon Gold Star [Dark Ray | Feint Attack]",
  "priceEur": 6303.25,
  "movedFromEur": 4412.27,
  "movedToEur": 6303.25,
  "changePct": 42.9,
  "direction": "up",
  "comparisonWindow": "since 2026-09-07",
  "isAlert": true,
  "previousSeenAt": "2026-09-07T06:00:00.000Z"
}
```

### The honest part: why most "movers" lists are noise

Cardmarket's averages are averages of **whatever actually sold** in the window. A card that trades a few times a month can swing 20% on nothing more than one sale being a German copy in played condition. Measured on the 2026-09-07 Pokémon guide: **about half of every priced card in the catalogue "moved" more than 15% on any window, at any price floor.** A tool that reports those is reporting nothing.

So by default this actor only reports a **corroborated** move, which means all of the following point the same way:

- the 30-day average, the 7-day average and today's trend form a ladder in the same direction;
- the **cheapest live offer** has already repriced to match - sellers have noticed too.

That keeps roughly one card in ten, and they are the ones a person would agree are moving. Turn `requireCorroboration` off if you want the raw comparison, and expect a much noisier list.

Two more guards worth knowing:

- **`minPriceEur` (default €2)** - a 2-cent common going to 4 cents is a 100% move and means nothing. The floor is what keeps bulk out.
- **Foils are separate rows.** A foil and its regular print are different products with different prices, and they often move independently.

### How change detection works (alerts mode)

- **Where the memory lives.** A named key-value store on *your* account (`stateStoreName`). It holds the last price this monitor reported per card variant. There is no server on my side holding your watchlist.
- **What the first run does.** It records today's prices for everything passing your filters and pushes nothing. From the next run you are told only about movement.
- **One price guide a day.** Cardmarket publishes a new file each morning. If you run the monitor twice in one day, the second run says so and charges nothing for the comparison, rather than pretending prices stood still.
- **What the memory can hold.** `maxWatched` (default 5,000 variants). When a whole game overflows it, the most valuable cards are kept - the ones a move actually matters on. A watchlist of names is always well under it.
- **Starting over.** `resetState: true` forgets the recorded prices and takes a fresh baseline.

### What this costs

Pay per outcome.

| Event           | Price                                | When it is charged                                         |
| --------------- | ------------------------------------ | ---------------------------------------------------------- |
| **Spike alert** | $0.0199 (from $0.0185 on paid plans) | A watched card that moved since the last run (alerts mode) |
| **Price mover** | $0.00499                             | A card in a movers list                                    |
| **Actor start** | $0.005                               | Per run, whatever it finds                                 |

Live prices are always the ones on this page's Pricing tab. A worked example: a daily watchlist of 200 cards costs the actor start on a quiet day - about 15 cents a month - plus 2 cents per genuine move. A weekly top-50 movers digest costs about 25 cents a run. New Apify accounts come with free monthly platform credits.

### Input reference

| Field                  | Type     | Default   | What it does                                                         |
| ---------------------- | -------- | --------- | -------------------------------------------------------------------- |
| `game`                 | string   | `pokemon` | One of 21 games on Cardmarket                                        |
| `mode`                 | string   | `movers`  | `movers` ranks what is moving now; `alerts` watches for future moves |
| `direction`            | string   | `gainers` | `gainers`, `losers` or `both`                                        |
| `window`               | string   | `7d`      | Which averages the headline change compares (`1d`, `7d`, `30d`)      |
| `minChangePct`         | number   | `15`      | Ignore smaller moves                                                 |
| `minPriceEur`          | number   | `2`       | Price floor - the main noise filter                                  |
| `maxPriceEur`          | number   | none      | Optional ceiling                                                     |
| `cardNames`            | string\[] | empty     | Partial names to watch; empty scans the whole game                   |
| `expansionIds`         | number\[] | empty     | Restrict to given expansions                                         |
| `includeFoil`          | boolean  | `true`    | Price foil variants as their own rows                                |
| `requireCorroboration` | boolean  | `true`    | Only report moves every average and the live offers agree on         |
| `maxWatched`           | integer  | `5000`    | Cap on remembered cards in alerts mode                               |
| `maxItems`             | integer  | `200`     | Stop after this many rows, biggest movers first                      |

### Scheduling and API use

```bash
curl -X POST "https://api.apify.com/v2/acts/lowlanddata~tcg-price-movers/runs?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"game":"pokemon","mode":"alerts","cardNames":["charizard","umbreon"],"minChangePct":10}'
```

Pull only the risers from a dataset:

```bash
curl "https://api.apify.com/v2/datasets/DATASET_ID/items?format=json&filter=direction%3Dup"
```

In n8n or Make: Schedule (daily, after 06:00 CET when the new guide is out) -> this actor in `alerts` mode -> Telegram or Slack.

### Use it with AI agents (MCP)

Every tool here doubles as an MCP endpoint: [tcg-price-movers/api/mcp](https://apify.com/lowlanddata/tcg-price-movers/api/mcp). A prompt that works as-is:

```text
Which Pokémon cards over 20 euros rose the most on Cardmarket this week,
and what are they trading at now?
```

### Sources and honest coverage notes

- **Cardmarket's own daily price guide and product catalog**, published as public files after the API closed to new applicants. One file per game, read directly - no login, no scraping, no rate limit.
- **Prices are Cardmarket's, so they are European sales in euros.** They do not track TCGplayer or eBay, and a card can move in the US without moving here. That difference is the point of the [TCG Price Arbitrage](https://apify.com/lowlanddata/tcg-price-arbitrage) tool.
- **Averages are of sales that happened.** A card nobody traded in 30 days has no `avg30`, and it cannot be judged a mover - it is skipped rather than guessed at.
- **One guide per day**, published overnight CET. Running more often than daily cannot show new movement.
- **Condition and language are not separated.** Cardmarket's averages mix them, which is exactly why the corroboration test exists.
- Sealed products are not covered here; this reads the singles catalog.

### FAQ

**Does it work on the first run?** In `movers` mode, yes - the price guide carries its own 1, 7 and 30-day averages, so momentum needs no history. `alerts` mode needs one baseline run first.

**Why is a card I know spiked missing?** Most likely the corroboration test: if the trend jumped but the week and month have not followed, or the cheapest offer has not repriced, it is treated as a blip. Set `requireCorroboration: false` to see it anyway.

**Can I watch just my collection?** Put the names in `cardNames` (partial matches work) or restrict to `expansionIds`. Both together narrow it further.

**Which window should I use?** `7d` for a weekly briefing, `30d` for slower structural moves, `1d` to catch something breaking today.

**Does it cover sealed boxes?** Not yet. Ask if you want it - the same files carry a sealed catalog.

**Is my watchlist private?** It lives in a key-value store on your own Apify account. I never see it.

### More tools from Lowland Data

| Tool                                                                           | What it does                                                      |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| [Cardmarket Price Guide](https://apify.com/lowlanddata/cardmarket-price-guide) | The full daily price table for a game - every card, every average |
| [Cardmarket Deal Finder](https://apify.com/lowlanddata/cardmarket-deal-finder) | Live offers priced under a card's own trend, condition-aware      |
| [TCG Price Arbitrage](https://apify.com/lowlanddata/tcg-price-arbitrage)       | The same card priced on Cardmarket (EU) against TCGplayer (US)    |

**Which one do I want?** This actor tells you *what changed*. The Price Guide gives you *everything, priced*. The Deal Finder finds *a copy to buy right now*. Full catalogue: [lowlanddata.com/catalogue](https://lowlanddata.com/catalogue/).

### Support

A card that should have shown up, a game you want added, sealed products? Open an issue on the **Issues** tab with the run ID, or write to <contact@lowlanddata.com>. Reports get read the same day.

# Actor input Schema

## `game` (type: `string`):

Which trading card game to scan. Pokemon is the busiest market on Cardmarket.

## `mode` (type: `string`):

Movers ranks the cards moving right now and works on the very first run. Alerts remembers today’s prices and tells you only when one of your cards moves later.

## `direction` (type: `string`):

Cards going up, going down, or both.

## `window` (type: `string`):

Which pair of averages the headline change compares.

## `minChangePct` (type: `integer`):

Ignore anything that moved less than this.

## `minPriceEur` (type: `integer`):

A 2-cent common doubling to 4 cents is a 100% move and means nothing. The floor is what keeps bulk out.

## `maxPriceEur` (type: `integer`):

Optional ceiling, for watching a budget bracket.

## `cardNames` (type: `array`):

Partial names, one per line: charizard, umbreon. Leave empty to scan the whole game.

## `expansionIds` (type: `array`):

Cardmarket expansion ids, as they appear in the output rows.

## `includeFoil` (type: `boolean`):

Foils are priced separately and often move on their own.

## `requireCorroboration` (type: `boolean`):

Requires the 30-day average, the 7-day average and today’s trend to all point the same way, with the cheapest live offer already repriced. Without it, roughly half the catalogue looks like it is moving.

## `maxWatched` (type: `integer`):

Caps the monitor memory. When a whole game overflows it, the most valuable cards are kept.

## `resetState` (type: `boolean`):

Forget the recorded prices and take a fresh baseline.

## `stateStoreName` (type: `string`):

Named key-value store holding the prices this monitor last reported.

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

Stop after this many rows. Biggest movers come first.

## Actor input object example

```json
{
  "game": "pokemon",
  "mode": "movers",
  "direction": "gainers",
  "window": "7d",
  "minChangePct": 15,
  "minPriceEur": 2,
  "cardNames": [],
  "includeFoil": true,
  "requireCorroboration": true,
  "maxWatched": 5000,
  "resetState": false,
  "stateStoreName": "tcg-movers-state",
  "maxItems": 200
}
```

# Actor output Schema

## `movers` (type: `string`):

One row per card variant that moved: the price now, what it moved from, the percentage, and the daily, weekly and monthly averages behind it.

# 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 = {
    "game": "pokemon",
    "mode": "movers",
    "direction": "gainers",
    "window": "7d",
    "cardNames": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("lowlanddata/tcg-price-movers").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 = {
    "game": "pokemon",
    "mode": "movers",
    "direction": "gainers",
    "window": "7d",
    "cardNames": [],
}

# Run the Actor and wait for it to finish
run = client.actor("lowlanddata/tcg-price-movers").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 '{
  "game": "pokemon",
  "mode": "movers",
  "direction": "gainers",
  "window": "7d",
  "cardNames": []
}' |
apify call lowlanddata/tcg-price-movers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lowlanddata/tcg-price-movers"
        }
    }
}

```

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/qePpF14MIfXBHdH2N/builds/9PnBC64VdzsuuRbaF/openapi.json
