# Polymarket Odds Monitor: Price Move Alerts (`wiseus-labs/polymarket-monitor`) Actor

Tracks Polymarket markets between runs and returns only the ones that moved: price changes past your thresholds, volume spikes, new markets. Pay per signal, not per row.

- **URL**: https://apify.com/wiseus-labs/polymarket-monitor.md
- **Developed by:** [Wiseus Labs](https://apify.com/wiseus-labs) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 price move signals

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

This Actor watches Polymarket and hands back only the markets where something
changed: a price move, a volume spike, or a brand new market, not the whole
list. You pay per signal, not per market checked, so a run that finds nothing
costs almost nothing beyond the Actor start fee. Every comparison is against
this Actor's own stored history over the window you choose, not against
Polymarket's own weekly change figure, and a market whose event has already
ended is skipped before a signal is even computed.

### What you get

A typical run checks around 300 of the roughly 17,000 markets Gamma lists,
the highest volume ones, and comes back with up to 5 signals, one row per
signal in the dataset. Markets whose end_date has already passed are
skipped, so a match that already finished playing out does not show up in
your results.

- **Price move**: the price moved enough during your comparison window to
  matter, not just drift. It must clear both a points and a percent
  threshold, 0.03 points and 15 percent by default.
- **Volume spike**: trading volume jumped and the price actually moved with
  it. A jump with no price movement is usually the 24 hour window rolling
  forward, not real trading, so it is not reported. Three thresholds must be
  met together, see the table below.
- **New market**: a market appeared since the previous run. Off by default,
  see Limits below.

The first run already returns price_move signals, computed from Polymarket's
own 1 hour price change. Volume spike and new market signals need a second
run, once the Actor has its own history to compare against.

Volume spike thresholds:

| Input | Default | Meaning |
| --- | --- | --- |
| `minVolumeSpikePct` | 25 | Minimum relative growth of 24 hour volume, in percent |
| `minVolumeSpikeAbs` | $5,000 | Minimum growth of 24 hour volume, in dollars |
| `minPriceMoveForVolumeSpike` | 0.01 | Minimum price move, in probability points, required alongside the volume growth. A volume spike with no price movement is usually the 24 hour rolling window shifting, not real trading. Set to 0 to report every volume spike regardless of price movement |

Every row carries the current and previous price, the change in points and in
percent, current and previous volume, liquidity, best bid, best ask, spread,
the market question, its tags, event title and a link to the market on
Polymarket.

### Example output

This is one row from the dataset, one paid signal:

```json
{
  "signal_type": "price_move",
  "condition_id": "0x6a94c71414b75c00edafa80ebae3a4fe44e624ce074a3bde6dd57c923d0a0d5f",
  "question": "Spread: Newcastle United FC (-1.5)",
  "outcome_yes": "Newcastle United FC",
  "price_now": 0.065,
  "price_then": 0.79,
  "price_change_points": -0.725,
  "price_change_pct": -91.77,
  "window_minutes": 60,
  "actual_window_minutes": 30.55,
  "window_source": "history",
  "volume_24h_now": 48140.8,
  "volume_24h_then": 35543.02,
  "volume_change_pct": 35.44,
  "liquidity": 2005.29,
  "best_bid": 0.03,
  "best_ask": 0.1,
  "spread": 0.07,
  "end_date": "2026-09-19T14:00:00Z",
  "market_url": "https://polymarket.com/event/epl-new-hul-2026-09-19-more-markets",
  "detected_at_iso": "2026-09-19T16:01:16.882491+00:00",
  "created_at_iso": "2026-09-06T05:00:45.089159Z",
  "age_hours": 323,
  "volume_direction": "down",
  "tags": ["sports", "games", "soccer", "premier-league", "EPL"],
  "event_slug": "epl-new-hul-2026-09-19-more-markets",
  "event_title": "Newcastle United FC vs. Hull City AFC - More Markets",
  "game_id": 90114111,
  "signal_strength": 24.17
}
```

`volume_spike` and `new_market` rows have the same structure, only
`signal_type` and which thresholds fired differ.

### How it works

The Actor keeps a short price and volume history in a named key-value store on
your account. Each run reads that history, compares it against live data, and
writes the history back.

The first run has no stored history to compare against, so it records a
baseline. It can still return `price_move` signals right away, computed from
Polymarket's own 1 hour price change, see Limits below. Volume spikes and new
market signals need a second run before they can fire.

When a run produces no signals, it writes a single free `run_summary` row
instead of leaving the dataset empty. That row tells you how many markets were
checked, how many candidates were suppressed, and why nothing fired. It is not
charged. Filter on `signal_type` if you only want real signals.

Set **Comparison window, minutes** close to how often you schedule the run. If
you run hourly, use 60. If you run every 15 minutes, use 15. The Actor looks
for the stored point closest to that window and reports the age it actually
used in `actual_window_minutes`, which is almost always a few minutes off the
window you asked for. That is the real interval the comparison was made over.

### Quick start

1. Leave the defaults and press Start. This run records a baseline and can
   already return `price_move` signals, see Limits below.
2. Wait roughly as long as your comparison window, then press Start again to
   get the full set of signal types.
3. When the output looks right, create a schedule with the same input and the
   same interval.

Results land in the dataset and can be sent onward through the Integrations
tab: webhooks, Slack, Zapier, Make, n8n and the rest.

### What a run costs

You pay per signal that reaches the dataset. Suppressed candidates, filtered
markets and `run_summary` rows are not charged. Platform costs are included
in the price of each signal, nothing extra is billed on top.

Price per signal depends on your Apify plan:

| Signal type | Free | Starter | Scale | Business |
| --- | --- | --- | --- | --- |
| Price move | $0.0150 | $0.0100 | $0.0080 | $0.0060 |
| Volume spike | $0.0150 | $0.0100 | $0.0080 | $0.0060 |
| New market | $0.0100 | $0.0070 | $0.0050 | $0.0040 |

**Max signals per run** is the ceiling, 5 by default. Signals are ranked by
strength first, so the cap keeps the strongest ones. That makes the worst
case of any single run:

`5 x $0.015 = $0.075` on Free, down to `5 x $0.006 = $0.03` on Business.

On an hourly schedule, that worst case works out to $54.00 a month on Free
and $21.60 a month on Business, with Starter and Scale in between. Most runs
return fewer than five signals, so the real figure is lower. Raise the cap
for full coverage, lower it to tighten the ceiling.

The Actor start fee is $0.00005 at 512 MB of memory, the same on every plan,
and one such fee per extra gigabyte above 1 GB.

### Tuning the output

Too many signals: raise **Minimum price move, percent**, raise **Minimum 24
hour volume** to work on a smaller and more liquid universe, raise **Min
price move for volume spikes** to drop volume spikes with no real price
movement, raise **Minimum hours until market end** to skip markets that are
about to conclude, or lower **Max signals per run**.

Too few signals: lower the move thresholds, lower **Minimum liquidity** and
**Minimum 24 hour volume**, or raise **Max markets per run**.

Check `signals_dropped_by_cap` on your rows. If it is regularly above 0, the
market is producing more qualifying signals per run than **Max signals per
run** lets through, and raising that cap will get you the rest of them.

Only the markets you care about: fill **Market slugs** or **Condition IDs** to
watch a fixed list, or use **Include tags** to keep a theme such as politics or
crypto. In watchlist mode the liquidity and volume filters do not apply, since
you named the markets yourself. The end_date filters still apply even in
watchlist mode.

### Limits worth knowing before you start

**Schedule interval and comparison window belong together.** If the interval is
much longer than the window, a market may have no usable history point. It
accepts points between a quarter and four times the window, so a missed run or
a few minutes of drift will not break the chain. When a market still has
nothing usable, price move signals fall back to Polymarket's own 1 hour price
change, see below. Volume spikes and new market signals have no such
fallback and simply will not fire for that market until it has a real
comparison point.

**Above 180 minutes** the Actor falls back to Polymarket's own 24 hour price
change until it has collected enough history of its own. In those rows
`window_source` is `gamma_1d` and `actual_window_minutes` is 1440, so the
comparison really is against 24 hours ago, not against the window you set.
Volume spikes have no such fallback and simply will not fire until history
builds up.

**When a market has no usable comparison point, price moves fall back to
Polymarket's own 1 hour price change.** This covers two cases: the very first
run, when there is no history at all yet, and any later run where a specific
market's own history has nothing recent enough, for example after a missed
schedule or a market that just fell outside the accepted age range. In both
cases the Actor reads Polymarket's `oneHourPriceChange` and can emit a
`price_move` signal from it, with `window_source` set to `gamma_1h` and
`actual_window_minutes` at 60. Check `window_source` on any row to see
whether the comparison came from the Actor's own stored history or from this
fallback. Volume spikes and new market signals have no such fallback and
simply will not fire until a market has a real comparison point.

**New market signals are off by default.** In a broad scan they mostly surface
sports sub-markets with no trading yet, which is noise you would be paying for.
Turn them on together with a watchlist or a tag filter.

**Markets near either end of the price scale are skipped.** By default a signal
is suppressed when the old or the new price sits outside 0.05 to 0.95, because
a market that is already settling produces large percentage moves that mean
nothing. Set **Minimum price for a signal** to 0 if you want to see those
collapses.

**A volume spike needs a price move too, not just a percent.** In testing,
about 64 percent of raw volume spikes moved the price by less than 0.01
points, usually the 24 hour window rolling forward rather than real trading.
Volume growth above 500 percent also looked dramatic on its own, but 72
percent of those rows started from under $5,000 in 24 hour volume, so the
percentage was inflated by a small base. **Min price move for volume spikes**
and **Minimum volume spike, dollars** filter both cases out.

**Watching by slug is more reliable than by condition ID.** A few markets
cannot be looked up by condition ID directly. The Actor logs them and carries
on with the rest.

**Auto-generated recurring markets are excluded**, such as the five-minute
crypto up or down contracts Polymarket creates around the clock. Turn them back
on with the checkbox if you want them.

**Markets whose end_date has already passed are always skipped**, for every
signal type, with no input to turn this off. A market past its listed end
date is settling, not repricing: its price sliding toward 0 or 1 reflects an
outcome that is already known, not new information worth paying for. Set
**Minimum hours until market end** above 0 to also skip markets that are
still open but about to conclude, for the same reason.

### Output fields

| Field | Meaning |
| --- | --- |
| `signal_type` | `price_move`, `volume_spike`, `new_market`, or `run_summary` for a run with no signals |
| `signal_strength` | How many times over its threshold the signal went. Used for ranking |
| `signals_dropped_by_cap` | How many signals this run's `maxSignalsPerRun` cap dropped. Same value on every row of the run, 0 if nothing was dropped. Not set on a `run_summary` row |
| `note` | On a `run_summary` row, why the run produced no signals |
| `markets_checked`, `candidates_suppressed` | On a `run_summary` row, how many markets passed the filters and how many candidates were suppressed |
| `volume_spikes_suppressed_min_price_move` | On a `run_summary` row, how many volume spike candidates were suppressed by `minPriceMoveForVolumeSpike` specifically. Counted within `candidates_suppressed` |
| `price_now`, `price_then` | Price at this run and at the comparison point, on the 0 to 1 scale |
| `price_change_points`, `price_change_pct` | Absolute and relative change |
| `volume_24h_now`, `volume_24h_then`, `volume_change_pct` | Same for 24 hour volume |
| `window_minutes`, `actual_window_minutes` | Requested window and the age of the point actually used |
| `window_source` | `history` for a stored point, `gamma_1d` for the long window fallback, `gamma_1h` for the 1 hour price change fallback, used on the first run and on any later run where a market has no usable history point |
| `liquidity`, `best_bid`, `best_ask`, `spread` | Order book context at the time of the signal |
| `question`, `slug`, `outcome_yes`, `end_date` | Market identity |
| `event_title`, `event_slug`, `tags`, `market_url` | Event context and link |
| `condition_id` | Stable market identifier, use it for a watchlist |
| `detected_at_iso`, `created_at_iso`, `age_hours` | When the signal fired and how old the market is |

### Running several monitors

All runs on your account share one history store by default, which is what you
want: change a threshold and the history stays. If you run two monitors with
different watchlists and want their histories kept apart, give each a different
**State key**.

# Actor input Schema

## `searchQueries` (type: `array`):

Keep only markets whose question text contains at least one of these words. Leave empty to check all markets that pass the other filters. Only used during a general market scan.

## `slugs` (type: `array`):

Watch specific markets by slug instead of scanning all markets. When this is filled in, the general market scan is skipped and only these markets are checked.

## `conditionIds` (type: `array`):

Watch specific markets by condition ID instead of scanning all markets. Works the same way as market slugs, and can be combined with them.

## `includeTags` (type: `array`):

Keep only events tagged with at least one of these tag slugs, for example politics or crypto. Leave empty to skip this filter. Only used during a general market scan.

## `excludeTags` (type: `array`):

Drop events tagged with any of these tag slugs. Only used during a general market scan.

## `minLiquidity` (type: `integer`):

Skip markets with liquidity below this value, in dollars. Not applied when watching specific markets by slug or condition ID.

## `minVolume24h` (type: `integer`):

Skip markets with traded volume in the last 24 hours below this value, in dollars. Not applied when watching specific markets by slug or condition ID.

## `windowMinutes` (type: `integer`):

How far back to compare price and volume against. Set this close to how often you schedule the actor to run. Above 180 minutes, if there is no usable history point yet, price move signals fall back to Polymarket's own 24 hour price change figure; volume spike signals have no such fallback and simply will not fire until enough history has accumulated.

## `minPriceMovePoints` (type: `number`):

Minimum absolute price change, on Polymarket's 0 to 1 price scale, needed to trigger a price move signal.

## `minPriceMovePct` (type: `number`):

Minimum relative price change, in percent of the starting price, needed to trigger a price move signal. Both this and the points threshold must be met.

## `minVolumeSpikePct` (type: `number`):

Minimum relative growth of 24 hour volume, in percent, needed to trigger a volume spike signal.

## `minVolumeSpikeAbs` (type: `integer`):

Minimum growth of 24 hour volume in dollars, needed to trigger a volume spike signal. Both this and the percent threshold must be met.

## `minPriceMoveForVolumeSpike` (type: `number`):

Minimum absolute price change, in probability points, required for a volume spike to be reported. Volume spikes without price movement usually mean the 24h rolling window shifted, not that the market repriced. Set to 0 to report all volume spikes.

## `minPriceForSignal` (type: `number`):

Suppress a price move or volume spike signal if price_now or price_then falls outside \[this value, 1 minus this value] on Polymarket's 0 to 1 price scale. Either price alone is enough to suppress the signal; both do not need to be outside the range. Does not apply to new market signals.

## `minHoursToEndDate` (type: `number`):

Skip markets that end within this many hours. Markets close to their end date often move because the outcome is already known, not because the market is repricing. Set to 0 to keep them.

## `repeatSuppressMinutes` (type: `integer`):

After a signal on a market, wait this many minutes before signaling on it again, unless the price moves further in that time.

## `newMarketMaxAgeHours` (type: `integer`):

Only signal a market as new if it was created less than this many hours ago.

## `newMarketMinLiquidity` (type: `integer`):

Minimum liquidity, in dollars, for the dedicated new market scan. Used instead of Minimum liquidity and Minimum 24 hour volume for that scan, since a market fresh off creation rarely clears those yet.

## `maxMarkets` (type: `integer`):

Maximum number of markets to check in one run. In a general scan, markets are taken in order of highest 24 hour volume first, up to this limit. When new market alerts are on, the dedicated new market scan reserves room within this same limit rather than adding to it. Not applied when watching specific markets by slug or condition ID.

## `maxSignalsPerEvent` (type: `integer`):

Maximum number of signals to keep from one real-world event in a run, the strongest first. When Polymarket splits one match into several events that share the same underlying game (for example a main line and a halftime result), they are grouped and capped together. Keeps one busy event from filling up the whole result list.

## `maxSignalsPerRun` (type: `integer`):

Hard cap on how many signals a single run can emit and charge for. Signals are ranked by strength before the cap is applied, so you always get the strongest ones first, regardless of signal type. Raise it if you want full coverage, lower it to keep the cost of every run predictable.

## `signalTypes` (type: `array`):

Which kinds of signals to look for. New market alerts are off by default. In a broad scan they mostly surface sports sub-markets that have no trading yet. Turn them on together with a watchlist or a tag filter, where they tell you about markets you actually care about.

## `includeAutoGeneratedMarkets` (type: `boolean`):

Polymarket creates short-lived recurring markets, such as five-minute crypto up or down contracts, around the clock. They are excluded by default because they produce a constant stream of low-value signals. Applies to the general scan and the dedicated new market scan; not applied when watching specific markets by slug or condition ID.

## `stateKey` (type: `string`):

Runs that share a state key share price history. Leave empty unless you run several separate monitors and want to keep their histories apart. Only letters, digits and hyphens are kept; anything else is dropped.

## Actor input object example

```json
{
  "searchQueries": [],
  "slugs": [],
  "conditionIds": [],
  "includeTags": [],
  "excludeTags": [],
  "minLiquidity": 1000,
  "minVolume24h": 5000,
  "windowMinutes": 60,
  "minPriceMovePoints": 0.03,
  "minPriceMovePct": 15,
  "minVolumeSpikePct": 25,
  "minVolumeSpikeAbs": 5000,
  "minPriceMoveForVolumeSpike": 0.01,
  "minPriceForSignal": 0.05,
  "minHoursToEndDate": 0,
  "repeatSuppressMinutes": 30,
  "newMarketMaxAgeHours": 24,
  "newMarketMinLiquidity": 100,
  "maxMarkets": 300,
  "maxSignalsPerEvent": 2,
  "maxSignalsPerRun": 5,
  "signalTypes": [
    "price_move",
    "volume_spike"
  ],
  "includeAutoGeneratedMarkets": false,
  "stateKey": ""
}
```

# 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 = {
    "searchQueries": [],
    "slugs": [],
    "conditionIds": [],
    "includeTags": [],
    "excludeTags": [],
    "minLiquidity": 1000,
    "minVolume24h": 5000,
    "windowMinutes": 60,
    "minPriceMovePoints": 0.03,
    "minPriceMovePct": 15,
    "minVolumeSpikePct": 25,
    "minVolumeSpikeAbs": 5000,
    "minPriceMoveForVolumeSpike": 0.01,
    "minPriceForSignal": 0.05,
    "minHoursToEndDate": 0,
    "repeatSuppressMinutes": 30,
    "newMarketMaxAgeHours": 24,
    "newMarketMinLiquidity": 100,
    "maxMarkets": 300,
    "maxSignalsPerEvent": 2,
    "maxSignalsPerRun": 5,
    "signalTypes": [
        "price_move",
        "volume_spike"
    ],
    "stateKey": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("wiseus-labs/polymarket-monitor").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 = {
    "searchQueries": [],
    "slugs": [],
    "conditionIds": [],
    "includeTags": [],
    "excludeTags": [],
    "minLiquidity": 1000,
    "minVolume24h": 5000,
    "windowMinutes": 60,
    "minPriceMovePoints": 0.03,
    "minPriceMovePct": 15,
    "minVolumeSpikePct": 25,
    "minVolumeSpikeAbs": 5000,
    "minPriceMoveForVolumeSpike": 0.01,
    "minPriceForSignal": 0.05,
    "minHoursToEndDate": 0,
    "repeatSuppressMinutes": 30,
    "newMarketMaxAgeHours": 24,
    "newMarketMinLiquidity": 100,
    "maxMarkets": 300,
    "maxSignalsPerEvent": 2,
    "maxSignalsPerRun": 5,
    "signalTypes": [
        "price_move",
        "volume_spike",
    ],
    "stateKey": "",
}

# Run the Actor and wait for it to finish
run = client.actor("wiseus-labs/polymarket-monitor").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 '{
  "searchQueries": [],
  "slugs": [],
  "conditionIds": [],
  "includeTags": [],
  "excludeTags": [],
  "minLiquidity": 1000,
  "minVolume24h": 5000,
  "windowMinutes": 60,
  "minPriceMovePoints": 0.03,
  "minPriceMovePct": 15,
  "minVolumeSpikePct": 25,
  "minVolumeSpikeAbs": 5000,
  "minPriceMoveForVolumeSpike": 0.01,
  "minPriceForSignal": 0.05,
  "minHoursToEndDate": 0,
  "repeatSuppressMinutes": 30,
  "newMarketMaxAgeHours": 24,
  "newMarketMinLiquidity": 100,
  "maxMarkets": 300,
  "maxSignalsPerEvent": 2,
  "maxSignalsPerRun": 5,
  "signalTypes": [
    "price_move",
    "volume_spike"
  ],
  "stateKey": ""
}' |
apify call wiseus-labs/polymarket-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,wiseus-labs/polymarket-monitor"
        }
    }
}
```

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/KcjxsTPgvP0XY4KBo/builds/tZu1YQOsqUNkyGFea/openapi.json
