# Polymarket API Scraper - Odds, Order Book, Trades, Wallets (`snow_leo_data/polymarket-scraper`) Actor

29,600 Polymarket markets in one sweep, where offset paging stops at offset 2,000. Up to 92 fields per market. Polymarket scraper for election odds and event odds: implied probability, volume, liquidity, top holders, trades and wallet positions, plus a Polymarket odds monitor.

- **URL**: https://apify.com/snow_leo_data/polymarket-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** News, Integrations, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 result rows

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

## Polymarket Prediction Market Scraper — Odds, Order Book, Trades & Wallets

Polymarket is a prediction market where every outcome trades between $0 and $1
and settles at one of them, so an outcome's price is the market's probability
for it. This Actor turns that into a dataset: one row per market, event, trade
or wallet position, with the prices already parsed into numbers and the event,
its tags and the order book joined in.

Everything below is measured, and every number is reproduced by
`tools/measure_source.py`, which writes `tools/measurements.json`. The
measurement date is 2026-09-12. The platform is live, so counts move day to day.

#### What does this Actor pull from Polymarket?

One run gives you any of four shapes:

- **markets** — one row per tradable market: outcomes with prices as implied
  probabilities, best bid and ask, spread, volume over several windows,
  liquidity, the event and its tags, and optionally the full order book, price
  history, top holders and the recent tape. Up to 92 fields.
- **events** — one row per event with its markets nested and the set priced as a
  whole: sum of outcome prices, overround, and the gap below $1 on a
  mutually exclusive set. 31 fields.
- **trades** — the platform-wide trade tape: every print with price, size,
  dollar notional, outcome, side and the wallet behind it. Measured: 2,000
  trades in 4 requests and 1.6 seconds.
- **wallets** — open positions for the addresses you list, each with entry
  price, current price, current value, realized and unrealized P\&L, plus the
  wallet's total portfolio value.

#### Why does ordinary pagination miss most of the catalogue?

Polymarket's catalogue API accepts `offset`, and that is the obvious way to page
through it. It stops working part-way: measured on 2026-09-12, `offset=2000`
still returns data and `offset=2001` returns HTTP 422 with the message *"offset
too large, use /events/keyset for deeper pagination"*. A scraper built on offset
paging therefore sees roughly the first 2,100 records and silently sees nothing
beyond them.

The keyset endpoints have no such ceiling — but only if you pass the cursor under
the name the API expects, which is `after_cursor`. This is the part that costs
people days: `cursor`, `next_cursor` and `after` are **not rejected**. The server
quietly returns the first page again, every time, with a fresh-looking cursor. A
sweep built that way looks like it is working, runs forever and collects the same
100 rows. Measured before the fix: 40 pages in a row, 100 unique records.

This Actor walks the keyset endpoints with the right cursor, so a sweep goes past
the ceiling and each page really is new.
A full sweep of open markets, measured the same day: **29,600 markets in 297 requests and 150.2 seconds** (stopped at the time limit, not the end of the catalogue), and the same walk over events gives **20,500 events in 206 requests and 150.0 seconds** (stopped at the time limit).
The live test suite asserts both halves of this: that the cursor advances, and
that the wrong cursor name silently does not.

#### Which filters does the API actually apply?

The same filter has different names on the two endpoints, and passing the wrong
one is not an error either — it is ignored:

| Filter | On markets | On events |
|---|---|---|
| Minimum volume | `volume_num_min` | `volume_min` |
| Minimum liquidity | `liquidity_num_min` | `liquidity_min` |

Measured: `volume_min=5000000000` on events returns an empty list, as it should,
while `volume_num_min` with the same threshold returns events with four thousand
dollars of volume — the filter was never applied. This Actor sends the right name
per endpoint **and** re-checks every threshold on the row itself, so the number
you type is the number you get.

#### What is in a row that the raw API does not give you?

Polymarket returns the three most important lists as **strings**: `outcomes` is
`'["Yes", "No"]'`, `outcomePrices` is `'["0.0395", "0.9605"]'`, and the CLOB
token ids arrive the same way. Money fields are strings too. Handed to you
unparsed, they mean JSON inside a CSV cell and a type error in your first
filter.

This Actor parses all of it and computes what the API does not:

| Field | What it is |
|---|---|
| `outcomes` | One object per outcome: name, price, implied probability, CLOB token id. |
| `impliedProbability` | Price of the first outcome — the answer to the question in the title. |
| `topOutcome`, `topOutcomePrice` | The leading outcome on a multi-outcome market. |
| `outcomePriceSum`, `overround`, `arbitrageGap` | What the whole set costs against the $1 that one winning outcome pays. |
| `payoutRatioTop` | Return per dollar if the leading outcome resolves true. |
| `midPrice`, `spreadPct` | Mid of bid and ask, and spread as a share of it. |
| `bidDepthUsd`, `askDepthUsd`, `bookImbalance` | Dollars resting on each side of the book and which side is heavier. |
| `volume24hShare`, `volumeToLiquidity` | How much of lifetime volume traded today, and today's volume against resting liquidity. |
| `hoursToEnd`, `daysToEnd`, `endsWithin24h` | Time left, precomputed. |
| `windowHigh`, `windowLow`, `windowChange` | Price range over the history window you chose. |
| `lastTradeAt`, `minutesSinceLastTrade`, `recentVwap`, `buyShare` | What the recent tape says about who is buying. |
| `topHolders`, `topHolderShareOfListed` | Largest holders per outcome and how concentrated they are. |

Empty numbers are `null`, never `""` — an empty string in a float column breaks
Excel, BigQuery and pandas.

#### How does the event-level pricing work?

On a mutually exclusive event — Polymarket calls it negative risk — exactly one
outcome can win, so the prices across its markets should add up to about $1. When
they add up to less, the whole set can be bought for less than the guaranteed
payout. `outcomePriceSum`, `overround` and `arbitrageGap` report that, and
`arbitrageGap` is filled only for events the platform actually marks as
mutually exclusive. On a set where the outcomes are not exclusive the sum means
nothing, and printing it as an edge would be worse than printing nothing.

#### How does monitor mode avoid paying for the same market twice?

Turn on **Monitor mode** and the Actor remembers what it delivered in a named
key-value store, which survives between runs — the default store does not, it is
wiped at the start of every run.

On the next run each market is classified `NEW`, `UPDATED` or `UNCHANGED`, and
unchanged markets are never written to the dataset, so they are never billed.
Every row also carries `probabilityChange`, the move since the last delivery.

**Min probability move** is what makes this worth having on a busy platform:
prices twitch constantly, so a plain content hash would call nearly every market
updated on every run. Set it to `0.05` and a market comes back only after its
price has moved five points. Tested in both directions: a 2-point move produces
zero rows, a 20-point move produces the full set again.

**State key** keeps several schedules apart; two schedules with different keys
never see each other's memory.

#### What can you filter before being billed?

Every filter runs before rows are written, so a filtered-out market never reaches
your bill:

- **Keyword** — substring over question, slug, description and event title.
- **Tag** — the platform's own tags: politics, crypto, sports, economy,
  elections and the rest. Applied by the API itself.
- **Market or event URLs** — paste links straight from the browser. A link with
  two path segments is a market; a link with one is an event, and its markets are
  expanded for you.
- **Min lifetime volume, min 24h volume, min liquidity** — in dollars, sent to
  the API where it supports them and re-checked on every row.
- **Min / max price of the first outcome** — `0.9` and up for near-certainties,
  `0.1` and below for longshots.
- **Ending within hours** — markets resolving soon.
- **Include closed markets** — off by default; turn it on for resolved history.
- **Sort by** — 24h volume, lifetime volume, liquidity, end date, start date or
  competitiveness, ascending or descending. With a limit set, sorting decides
  *which* markets you get, so it is a filter in practice.

#### What does the output look like?

A market row, abridged:

```json
{
  "marketId": "2252244",
  "conditionId": "0xa3b36b2d6104d34af4e6c6215fc818e43352e78a748fbfb0b85e3a35f71dec9a",
  "slug": "will-there-be-no-change-in-fed-interest-rates-after-the-september-2026-meeting-615",
  "question": "Will there be no change in Fed interest rates after the September 2026 meeting?",
  "groupItemTitle": "No change",
  "url": "https://polymarket.com/event/fed-decision-in-september-762/will-there-be-no-change-in-fed-interest-rates-after-the-september-2026-meeting-615",
  "eventId": "481717",
  "eventTitle": "Fed Decision in September?",
  "tags": ["Fed Rates", "Economy", "Politics"],
  "outcomes": [
    {"outcome": "Yes", "price": 0.945, "impliedProbability": 0.945, "tokenId": "9123..."},
    {"outcome": "No", "price": 0.055, "impliedProbability": 0.055, "tokenId": "4471..."}
  ],
  "impliedProbability": 0.945,
  "topOutcome": "Yes",
  "outcomePriceSum": 1.0,
  "overround": 0.0,
  "bestBid": 0.944,
  "bestAsk": 0.947,
  "midPrice": 0.9455,
  "spread": 0.003,
  "volume24hr": 1607864.0,
  "liquidity": 221725.52,
  "volumeToLiquidity": 7.25,
  "endDate": "2026-09-16T12:00:00Z",
  "hoursToEnd": 118.6,
  "endsWithin24h": false,
  "bidDepthUsd": 118958.21,
  "askDepthUsd": 91240.77,
  "bookImbalance": 0.13,
  "source": "polymarket.com",
  "scrapedAt": "2026-09-12T01:18:53Z"
}
```

A market row carries 70 fields without enrichment and 92 fields with the order
book, price history, holders and tape switched on. Compact output reduces it to
the 17 fields a decision is made on.

#### How much does it cost to run?

Pricing is pay per event: **$0.001 when a run starts** and **$0.0005 per row**
written to the dataset, which is **$0.50 per 1,000 rows**. Platform usage is
included in that price — compute is not billed to you separately.

| Rows in a run | What you pay |
|---|---|
| 100 | $0.051 |
| 1,000 | $0.501 |
| 10,000 | $5.001 |

Filters and monitor mode both cut rows before they are written, so both cut the
bill directly.

#### How do you use this for a scheduled job?

A working setup for hourly monitoring of one topic:

1. Set **Tag** to the topic you follow, e.g. `politics`.
2. Set **Min 24h volume** to a level that removes dead markets, e.g. `1000`.
3. Turn on **Monitor mode**, set **Min probability move** to `0.05`, give the
   schedule its own **State key**.
4. Optionally set a **Webhook URL** — Slack and Discord incoming webhooks are
   formatted for them automatically, any other URL receives plain JSON with the
   run summary and up to 20 rows.

The first run builds the baseline and delivers everything that matches. Later
runs deliver only markets that moved past your threshold.

#### What can this Actor not do?

Named honestly, because the comparison matters more than the sales pitch:

- **No Telegram or WhatsApp delivery.** Those need bot tokens and phone-number
  ids; only webhook-style delivery (generic, Slack, Discord) is implemented.
- **No MCP or Notion connector.** Results come out through the dataset, the Apify
  API and webhooks.
- **No raw API blob per row.** Every field is parsed and typed instead; the
  `conditionId` and `marketId` in each row are enough to fetch the untouched
  record yourself.
- **No trading, no account data.** This Actor reads only public endpoints — no
  key, no proxy, no browser — so it cannot place orders or read a private
  account. Wallet mode reads what the chain publishes about any address.
- **Order book, history, holders and tape cost extra API requests per returned
  market** — the order book costs one per outcome, so two on a binary market.
  They are off by default for that reason.
- **No profit-and-loss time series for a wallet.** Wallet mode returns the
  positions a wallet holds now, with realized and unrealized P\&L on each, not a
  P\&L curve over a chosen window.
- **Price history covers the first outcome.** On a binary market the second
  outcome is its complement; on a multi-outcome market you get the history of the
  first listed outcome only.

#### How is this checked?

Four suites, all runnable from the project:

- `python3 tests/test_logic.py` — parsing, arithmetic, filters, memory and
  shaping on recorded API responses, no network.
- `python3 tests/test_live.py` — the live API: the cursor advances, the wrong
  cursor name silently does not, the offset ceiling is where we say it is, server
  filters really filter.
- `python3 tests/test_lifecycle.py` — a run broken in the middle, resumed,
  repeated; memory between runs; the spending limit; webhook delivery.
- `python3 tools/verify_readme.py` — every number in this file against the code
  and against `tools/measurements.json`.

The ordering rule that matters for your data: rows are written to the dataset
first and only then marked as delivered in memory. The reverse order loses rows
silently when a run is interrupted, and the lifecycle suite fails on purpose if
anyone swaps it back.

### FAQ

#### Do you need a Polymarket account or API key?

No. The Actor reads Polymarket's public APIs — the Gamma catalogue, the CLOB
order book and the public data API. No key, no login, no proxy, no browser.

#### How current is the data?

Each row is a snapshot taken during the run, and `scrapedAt` records the moment.
For tighter tracking, filter to one tag or a handful of markets and run more
often; for a whole-catalogue picture, run without filters and accept that the
sweep takes minutes rather than seconds.

#### Can you get closed and resolved markets?

Yes. Turn on **Include closed markets**. Closed rows keep their last prices and
their end date, which is what backtesting needs. For price paths, turn on
**Include price history** and pick a window up to `max`.

#### How do you find a market or event?

Paste its polymarket.com URL into **Market or event URLs**. An event link expands
into its markets; a market link returns just that market. No ids to look up.

#### What does `arbitrageGap` mean, exactly?

On a mutually exclusive event, the winning outcome pays $1. If the prices of all
outcomes add up to less than $1, the difference is what a buyer of the whole set
would keep. That difference is `arbitrageGap`. It ignores fees, slippage and the
size actually available at those prices, so treat it as a screen, not a trade.

#### What happens when a run hits your spending limit?

The platform stops charging at the limit but does not stop the run. This Actor
reads the remaining budget at the start and stops writing rows once it is used
up, so you never get a half-billed run, and the run report says
`chargeLimitReached`.

#### Can the output be used by an AI agent?

Yes. **Compact output** returns 17 core fields instead of 92, **Description max
length** trims the long resolution text, and **Drop empty fields** removes nulls
and empty arrays. Together they cut the payload enough to fit many markets into a
context window.

#### Is scraping Polymarket allowed?

The Actor reads only public API endpoints, the same ones the website uses, and
does not log in or bypass anything. Wallet addresses, positions and trades are
public blockchain data, and the display names attached to them are published by
the platform. You are responsible for complying with Polymarket's terms and with
the rules that apply to you. This Actor is not affiliated with or endorsed by
Polymarket.

# Actor input Schema

## `query` (type: `string`):

Case-insensitive substring matched against the question, the slug, the description and the event title, e.g. fed, election, bitcoin. Leave empty to take the busiest markets first - with a keyword the Actor has to walk the catalogue until it finds enough matches, which takes longer on a rare word.

## `resultType` (type: `string`):

markets - one row per tradable market (default). events - one row per event with its markets nested and the set priced as a whole. trades - the exchange-wide trade tape. wallets - open positions and P\&L for the wallet addresses you list.

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

Stop after this many rows. 0 means no limit: a full sweep of open markets is 29,600 rows (measured 2026-09-12). A run with no limit AND no filters stops at 100 rows, so an accidental full sweep cannot surprise you; with any filter set, 0 really means everything that matches.

## `tagSlug` (type: `string`):

Server-side filter by Polymarket tag, e.g. politics, crypto, sports, economy, elections. One tag per run.

## `targets` (type: `array`):

Paste polymarket.com links straight from the browser. A link with two path segments is a market, a link with one is an event and its markets are expanded. Slugs work too.

## `wallets` (type: `array`):

Used by wallet mode: 0x... proxy wallet addresses. Each returns its open positions with entry price, current value and P\&L, plus the portfolio value.

## `includeClosed` (type: `boolean`):

Off by default: closed markets no longer trade. Turn on for resolved history and backtesting.

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

Server-side ordering. Volume 24h first is what you want for a sample of what is actually trading right now.

## `ascending` (type: `boolean`):

Off means biggest first.

## `minVolume` (type: `integer`):

Applied by the API itself where it supports it, and re-checked here, so the number you set is the number you get.

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

Dollars traded in the last 24 hours.

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

Dollars resting in the order book.

## `minPrice` (type: `string`):

Lower bound, 0 to 1. Use 0.9 for near-certain outcomes.

## `maxPrice` (type: `string`):

Upper bound, 0 to 1. Use 0.1 to list longshots only.

## `endingWithinHours` (type: `integer`):

Keep only markets that resolve inside this many hours from now.

## `includeOrderBook` (type: `boolean`):

Full resting book for every outcome, plus computed bid and ask depth in dollars and the imbalance between them. Costs one extra API request per outcome, so two per binary market.

## `includePriceHistory` (type: `boolean`):

Time and price pairs for the first outcome over the chosen window, plus the window high, low and net change.

## `priceHistoryInterval` (type: `string`):

How far back the history reaches.

## `includeTopHolders` (type: `boolean`):

Largest position holders per outcome, by size, with the public wallet address and display name.

## `topHoldersLimit` (type: `integer`):

How many holders to keep per market.

## `includeRecentTrades` (type: `boolean`):

Last trades on the market, plus time since the last trade, recent VWAP and the buy-side share.

## `recentTradesLimit` (type: `integer`):

How many recent trades to fetch per market.

## `descriptionMaxLength` (type: `integer`):

Trim the resolution description to this many characters. 0 keeps it whole. Polymarket descriptions run long, and this is the cheapest way to cut payload for LLM pipelines.

## `onlyChanged` (type: `boolean`):

Remember what was delivered and emit only markets that are new or that actually moved. State lives in a named key-value store, so it survives between runs.

## `minProbabilityMove` (type: `string`):

In monitor mode, ignore moves smaller than this. 0.05 means a market is re-emitted only after its price moves 5 points.

## `emitUnchanged` (type: `boolean`):

Off by default: you should not pay twice for a market that did not move.

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

Separate memories for separate schedules. Two schedules with different keys do not interfere.

## `compactOutput` (type: `boolean`):

17 core fields instead of the full row - for LLM context windows and agent pipelines.

## `excludeEmptyFields` (type: `boolean`):

Remove null, empty-string and empty-array fields from each row before it is written.

## `webhookUrl` (type: `string`):

A run summary with up to 20 rows is POSTed here when the run ends. Slack and Discord incoming webhooks are formatted for them automatically; any other URL gets plain JSON.

## Actor input object example

```json
{
  "resultType": "markets",
  "maxItems": 100,
  "targets": [],
  "wallets": [],
  "includeClosed": false,
  "sortBy": "volume24hr",
  "ascending": false,
  "includeOrderBook": false,
  "includePriceHistory": false,
  "priceHistoryInterval": "1d",
  "includeTopHolders": false,
  "topHoldersLimit": 10,
  "includeRecentTrades": false,
  "recentTradesLimit": 20,
  "descriptionMaxLength": 0,
  "onlyChanged": false,
  "minProbabilityMove": "0",
  "emitUnchanged": false,
  "stateKey": "default",
  "compactOutput": false,
  "excludeEmptyFields": false
}
```

# Actor output Schema

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

All collected rows

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/polymarket-scraper").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/polymarket-scraper").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 '{}' |
apify call snow_leo_data/polymarket-scraper --silent --output-dataset

```

## MCP server setup

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

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/ZagnWuNdDpMSxdw9E/builds/otx1xnLcu5i3X1Un7/openapi.json
