# Prediction Market Odds Scraper (Polymarket & Kalshi) (`romajure/prediction-market-odds`) Actor

Scrape Polymarket and Kalshi prediction market odds: 0-1 prices, implied probability, bid/ask, volume, liquidity and orderbook depth. No login. Pay per market.

- **URL**: https://apify.com/romajure/prediction-market-odds.md
- **Developed by:** [Romas Jurevicius](https://apify.com/romajure) (community)
- **Categories:** AI, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 market scrapeds

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

### Prediction Market Odds Scraper (Polymarket & Kalshi)

Prediction Market Odds turns the two largest prediction exchanges into one clean, machine-readable feed. It returns one JSON record per market - "Fed cut in December?", "Bitcoin above $110k on October 11?", "Who will be the next NATO Secretary General?", sports and esports matchups - with every price normalized to a **0-1 probability** (`0.0565` = 5.65%), the implied probability in percent, the best bid and ask, the last trade, lifetime and 24-hour volume, resting liquidity, open interest, the close date, the resolution rules and the exchange's own URL. Polymarket and Kalshi rows have the same shape, so a Kalshi contract at 6 cents and a Polymarket share at $0.06 compare directly. No account, API key or login is needed: both exchanges publish this data through official public endpoints.

Typical uses: feeding an AI trading agent that cross-references macro odds (rate decisions, CPI prints, elections) before placing trades, scanning for price disagreements between Polymarket and Kalshi on the same question, tracking a watchlist of markets every few minutes into a database, alerting a Telegram or Slack channel when a probability crosses a threshold, or backfilling a research dataset of resolved markets with their final prices.

Two sources are supported and can be combined in one run:

- **`polymarket`** - the Gamma API (`gamma-api.polymarket.com`) for events, markets and tags, plus the CLOB API (`clob.polymarket.com`) for orderbooks. Prices are USDC per share, already 0-1.
- **`kalshi`** - the trade API v2 (`api.elections.kalshi.com/trade-api/v2`) for events, markets and orderbooks. Prices are published in dollars per $1 contract and arrive as 0-1 floats; legacy integer-cent fields are converted too.

### How to scrape prediction market odds

1. Open the Actor in Apify Console and click **Try for free**.
2. Pick the **Sources** (default: both) and the **Market status** (default: `active`, i.e. tradeable now; `closed` adds resolved and settled markets; `all` for both).
3. Narrow the output with **Search** (`bitcoin`, `fed rate`, `nato`), **Polymarket tags** (`crypto`, `politics`, `fed`) or **Kalshi series tickers** (`KXBTCD`, `KXFED`, `KXCPI`). Filtering happens before anything is charged.
4. Set **Max markets per source** (default 30) and turn on **Include orderbook** if you need depth beyond the top of book.
5. Click **Start**. Results appear in the **Output** tab within seconds and can be downloaded as JSON, CSV or Excel, or read through the API.

#### How the fetching works

For Polymarket the Actor lists events with their nested markets from `GET /events` (100 per page - the API caps `limit` there - ordered by 24-hour volume for active markets and by end date for closed ones), optionally one request series per tag slug, and pages with `offset` until the cap or the end of the list. Each nested market carries its outcomes, quoted prices, best bid/ask, last trade, volume and liquidity, so no second request is needed per market. With **Include orderbook** on, the CLOB `POST /books` endpoint is called with batches of 20 token ids, and the best bid/ask are refreshed from the live book.

For Kalshi, active and closed markets come from `GET /events?with_nested_markets=true` (200 events per page, with category and settlement sources), paged with the API's cursor; settled markets come from `GET /markets?status=settled` (1,000 per page) because the events listing does not nest markets once an event has settled. Multivariate "parlay" combinations (`mve_collection_ticker`) are skipped: they carry no odds of their own. Each market's quote is read from `yes_bid`/`yes_ask`/`no_bid`/`no_ask`; the orderbook endpoint is called once per market when **Include orderbook** is on.

Requests are serialized with a 250 ms gap and retried on network errors, 429 and 5xx responses. Markets are pushed most-traded first, and every market id is remembered in the run state so a migrated or resurrected run never charges the same market twice.

### Prediction Market Odds Scraper pricing

This Actor uses Apify's **pay-per-event** model. There is no subscription and no per-result platform fee on top:

- **Actor start**: a flat fee of $0.00005 per run.
- **Market scraped**: **$0.001 per market** pushed to your dataset ($1 per 1,000 markets).
- **Orderbook fetched**: **$0.0005 per market** whose orderbook was fetched (only with **Include orderbook** on, and only when the exchange returned a book).

Search, status, tag and series filters are free and applied before charging - a run for `search: "fed"` on both exchanges is typically 20-60 markets, well under $0.10. Status records for a source that was unavailable or returned nothing are free. You can cap spending with **Maximum cost per run**; when the cap is reached the Actor stops cleanly and keeps everything scraped so far.

### Input example

```json
{
    "sources": ["polymarket", "kalshi"],
    "status": "active",
    "search": "bitcoin",
    "tags": ["crypto"],
    "seriesTickers": ["KXBTCD"],
    "maxMarketsPerSource": 200,
    "includeOrderbook": false,
    "proxyConfiguration": { "useApifyProxy": true }
}
```

#### Input fields

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `sources` | array of `polymarket` / `kalshi` | both | Which exchanges to read. Each record carries its `source`. |
| `status` | `active` / `closed` / `all` | `active` | `active` = tradeable now; `closed` = trading ended, including resolved/settled; `all` = both. |
| `search` | string | none | Case-insensitive filter on question, event title, slug, ticker and event/series ticker. All words must appear. |
| `tags` | array of strings | none | Polymarket only: event tag slugs such as `crypto`, `politics`, `fed`, `bitcoin`, `sports`. One request series per tag. |
| `seriesTickers` | array of strings | none | Kalshi only: series such as `KXBTCD`, `KXFED`, `KXCPI`, `KXNEXTNATOSECGEN`. |
| `maxMarketsPerSource` | integer | `30` | Stop a source once this many markets matched the filters (max 20,000). |
| `includeOrderbook` | boolean | `false` | Fetch live books: top 10 levels per outcome, depth, resting dollars; refreshes best bid/ask. |
| `proxyConfiguration` | object | Apify Proxy | Standard Apify proxy settings. |

### Output example

One dataset item per market (Kalshi example, captured 2026-10-11):

```json
{
    "source": "kalshi",
    "marketId": "KXNEWPOPE-70-LANT",
    "eventId": "KXNEWPOPE-70",
    "eventTitle": "Who will the next Pope be?",
    "question": "Who will the next Pope be? (Luis Antonio Tagle)",
    "slug": null,
    "ticker": "KXNEWPOPE-70-LANT",
    "seriesTicker": "KXNEWPOPE",
    "url": "https://kalshi.com/markets/kxnewpope/who-will-the-next-pope-be/kxnewpope-70",
    "outcomes": [
        { "name": "Yes", "price": 0.0565, "impliedProbabilityPct": 5.65, "bestBid": 0.041, "bestAsk": 0.072, "tokenId": null },
        { "name": "No", "price": 0.9435, "impliedProbabilityPct": 94.35, "bestBid": 0.928, "bestAsk": 0.959, "tokenId": null }
    ],
    "isBinary": true,
    "yesPrice": 0.0565,
    "noPrice": 0.9435,
    "lastTradePrice": 0.04,
    "spread": 0.031,
    "volumeUsd": 21292.58,
    "volume24hUsd": 1376.83,
    "volumeContracts": 21292.58,
    "volume24hContracts": 1376.83,
    "liquidityUsd": null,
    "openInterest": 4703.57,
    "status": "active",
    "sourceStatus": "active",
    "resolvedOutcome": null,
    "createdAt": "2025-07-18T08:05:49.322Z",
    "endDate": "2070-01-01T15:00:00.000Z",
    "resolutionSource": "The New York Times; the Associated Press; Reuters; Axios; Politico; ...",
    "description": "If Luis Antonio Tagle becomes the first person elected Pope before Jan 1, 2070, then the market resolves to Yes.",
    "category": "Elections",
    "tags": [],
    "orderbookFetched": false,
    "scrapedAt": "2026-10-11T05:49:05.178Z"
}
```

A Polymarket record has the same keys: `marketId` is the Gamma market id, `eventId` the Gamma event id, `slug` the URL slug, `ticker`/`seriesTicker` are `null`, each outcome carries its CLOB `tokenId`, `tags` holds the event's tag slugs (`["bitcoin", "crypto", "weekly"]`) and `category` is `null`.

Field notes:

- **Prices are 0-1 everywhere.** `outcomes[].price` is the exchange's quoted price: Polymarket's `outcomePrices`, or on Kalshi the midpoint of the Yes book with 0 / 1 standing in for an empty side (the same convention Polymarket uses). `impliedProbabilityPct` is `price x 100`. `bestBid`/`bestAsk` are `null` when that side of the book is empty - the Actor never invents a quote.
- `yesPrice` / `noPrice` are filled only for Yes/No questions. Polymarket matchups with team names as outcomes (`["MOUZ", "Spirit"]`) keep them `null`; read `outcomes` instead. `isBinary` is `true` for any two-outcome market.
- `status` is normalized: `active` (tradeable), `closed` (trading ended, not yet resolved), `resolved` (settled; `resolvedOutcome` names the winner and the winning outcome's price is `1`), `upcoming` (listed, not yet open). `sourceStatus` keeps the exchange's own label (`finalized`, `determined`, `closed/resolved`, ...).
- **Volume.** Polymarket reports USDC traded. Kalshi reports contract counts; `volumeUsd` / `volume24hUsd` are those counts times the $1 notional per contract (the raw counts stay in `volumeContracts` / `volume24hContracts`). Treat Kalshi's figure as notional, not dollars that changed hands.
- `liquidityUsd` is Polymarket's resting liquidity figure. Kalshi does not publish one in the listing, so it is `null` unless **Include orderbook** is on, in which case it is the dollars resting on both sides of the fetched book (Yes bids pay `p`, No bids pay `1 - p`).
- `openInterest` is Kalshi's open contracts; Polymarket publishes open interest only per event, so it is `null` there.
- With **Include orderbook** on, each outcome gets `orderbook: { bids, asks, bidDepth, askDepth, restingUsd }` with up to 10 levels per side, best first, prices in 0-1 and sizes in shares (Polymarket) or contracts (Kalshi). On Kalshi the asks of Yes are the mirrored No bids (`1 - price`) and vice versa, which is exactly how the exchange matches orders. `orderbookFetched` tells you whether the book came back; only those markets are charged the orderbook event.
- `url` is the market page. Kalshi pages have the form `kalshi.com/markets/{series}/{slug}/{event}`; the middle segment is cosmetic and the Actor builds it from the event title.
- `resolutionSource` is Polymarket's resolution source text, or Kalshi's settlement sources joined with `; `. `description` holds Polymarket's resolution text or Kalshi's primary rules.

A source that could not be read produces a single free record with `status: "unavailable"` (or `"empty"`) and a `message` containing the HTTP status and a body snippet, so your pipeline can tell "no markets" from "exchange down".

### Integrations

- **API**: start runs and read results with the [Apify API](https://docs.apify.com/api/v2) or the JavaScript and Python clients. Run the Actor synchronously with `run-sync-get-dataset-items` to get a fresh odds snapshot in one call.
- **MCP / AI trading agents**: the Actor is exposed through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp) at `https://mcp.apify.com`. Add it to Claude Desktop, Claude Code, Cursor, ChatGPT or any MCP-capable trading agent, enable this Actor as a tool, and the agent can call it with the same input - for example `{"search": "fed", "status": "active"}` before an FOMC decision, or `{"seriesTickers": ["KXBTCD"], "includeOrderbook": true}` to read the Bitcoin strike ladder with depth. Because prices are already probabilities, the agent can compare them with its own model or with the other exchange without any unit conversion. No custom MCP server is needed; the Actor's input schema becomes the tool schema.
- **n8n and Make**: use the Apify node or module, pick this Actor, pass the input JSON above and map the dataset items into your workflow.
- **Schedules and webhooks**: run it every few minutes from Apify Schedules and let a webhook push the snapshot to Slack, Telegram, Google Sheets, Postgres or your trading system.

### Sources and limitations

**Polymarket (`polymarket`).** Read from the public Gamma and CLOB APIs. Verified on 2026-10-11: `GET /events?active=true&closed=false&limit=100` returned 100 events with 1,242 nested markets, `tag_slug=crypto` worked, `POST /books` returned full books, and `limit` values above 100 were silently capped at 100. Gamma's `/markets` endpoint does not carry tags, which is why the Actor lists through `/events`. Polymarket markets are always two-outcome (multi-choice questions are events with one market per choice), so `isBinary` is always `true` there. `openInterest` is not published per market. The quoted `outcomePrices` can lag the live book by a few seconds; turn on **Include orderbook** when you need the exact top of book.

**Kalshi (`kalshi`).** Read from the public trade API v2, which serves market data without authentication (trading endpoints need keys and are not used). Verified on 2026-10-11: `/events?status=open&with_nested_markets=true&limit=200` returned 200 events with 1,821 markets, `series_ticker=KXBTCD` returned 318 open strike markets, `/markets/{ticker}/orderbook` returned `orderbook_fp` levels, and `/events?status=settled` returned events without nested markets (hence the `/markets?status=settled` path). The exchange lists markets in its own order, not by activity, so a small `maxMarketsPerSource` without filters returns whatever Kalshi lists first; use `seriesTickers` or `search` to target what you need. Settled markets fetched from `/markets` have no event metadata: `category` is `null` and `eventTitle` falls back to the market title. Multivariate parlay legs are excluded. Kalshi's public rate limit is generous for this volume; the Actor still spaces requests 250 ms apart.

**Both** exchanges publish live data that changes by the second; `scrapedAt` tells you when the record was taken. Prices on illiquid markets (wide spreads, zero 24-hour volume) are quotes, not fair values - check `spread`, `volume24hUsd` and `liquidityUsd` before acting on them.

### Prediction Market Odds Scraper FAQ

#### How do I compare the same question across both exchanges?

Run with `search` set to the topic (`"fed december"`, `"bitcoin 100k"`) and both sources on. Prices are 0-1 on both, so the difference between `yesPrice` values is the raw edge before fees and slippage.

#### Why is `yesPrice` null on some Polymarket markets?

Those markets name their outcomes (`["Texas", "Oklahoma"]`) rather than Yes/No. The prices are in `outcomes`; `yesPrice` is only filled when the outcomes are literally Yes and No so it always means the same thing.

#### Can I get resolved markets with their final prices?

Yes - set `status: "closed"`. Resolved markets carry `status: "resolved"`, `resolvedOutcome` and a winning price of `1`.

#### How much does a full snapshot cost?

At the default 30 markets per source, a run of both exchanges is 60 markets, i.e. $0.06, plus $0.03 if orderbooks are on. At 200 markets per source it is 400 markets, i.e. $0.40, plus $0.20 with orderbooks.

#### What happens if an exchange is down?

The run still succeeds with the other source, and a free `unavailable` record explains what failed (HTTP status and body snippet). Nothing is faked.

### Related actors

Same author, same pay-per-event model and the same no-login approach. They combine well in one pipeline (for example calendar events plus prediction odds plus channel chatter around a release):

- [Telegram Channel Scraper](https://apify.com/romajure/telegram-channel-scraper) - posts, views, media and links from public Telegram channels, no login.
- [Substack Scraper](https://apify.com/romajure/substack-scraper) - posts, authors, likes, restacks and comments from public Substack publications, custom domains included.
- [Economic Calendar Scraper](https://apify.com/romajure/economic-calendar) - Forex Factory and Investing.com macro events with UTC times, impact, actual, forecast and previous values.
- [Product Hunt Scraper](https://apify.com/romajure/product-hunt-scraper) - daily, weekly and monthly leaderboards, topic pages, search and product launches with votes, makers and website URLs.

### Legal and compliance

This Actor only reads public, unauthenticated API endpoints that both exchanges document for market data, and it never places orders or touches account endpoints. Market prices are public facts, but each exchange's presentation and API are subject to its own terms: see [Polymarket's terms of use](https://polymarket.com/tos) and [Kalshi's terms](https://kalshi.com/terms). You are responsible for using the data in line with those terms and applicable law, including any restrictions on redistribution and on trading from your jurisdiction. Keep request rates low (the Actor already serializes requests with a delay) and read Apify's article on the [legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) if you are unsure. Nothing here is investment advice.

### Support

Found a market that normalizes wrongly, a missing field, or need another exchange (Manifold, PredictIt, Metaculus)? Open an issue on the Actor's **Issues** tab in Apify Console or contact the author through the Actor page. Reports that include the input JSON and the market id or ticker are usually fixed within days.

# Actor input Schema

## `sources` (type: `array`):

Which exchanges to read. Both use public, login-free endpoints: Polymarket's Gamma API (events, markets, tags) plus the CLOB API for orderbooks; Kalshi's trade API v2 (events, markets, orderbooks).

## `status` (type: `string`):

`active`: open, tradeable markets (default). `closed`: markets that stopped trading, including resolved/settled ones. `all`: both.

## `search` (type: `string`):

Optional case-insensitive text filter on the market question, event title, slug and ticker. Several words must all appear (`fed rate` keeps "Fed rate cut in December?"). Filtering happens before charging.

## `tags` (type: `array`):

Polymarket only: restrict to events carrying these tag slugs, e.g. `crypto`, `politics`, `bitcoin`, `fed`, `sports`. One request series per tag. Ignored for Kalshi.

## `seriesTickers` (type: `array`):

Kalshi only: restrict to these series tickers, e.g. `KXBTCD` (hourly Bitcoin price), `KXFED` (Fed decisions), `KXCPI`. Ignored for Polymarket.

## `maxMarketsPerSource` (type: `integer`):

Stop each source after this many markets have matched the filters. Markets are listed most-active first on Polymarket and in exchange order on Kalshi.

## `includeOrderbook` (type: `boolean`):

Fetch the live orderbook for every pushed market: best bid/ask refreshed from the book plus up to 10 depth levels per outcome. One extra request per market on Kalshi, one per 20 markets on Polymarket; charged as `orderbook-fetched`.

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

Proxy settings. Apify Proxy is recommended so the exchanges see a normal datacenter session and rate limits are spread across IPs.

## Actor input object example

```json
{
  "sources": [
    "polymarket",
    "kalshi"
  ],
  "status": "active",
  "tags": [],
  "seriesTickers": [],
  "maxMarketsPerSource": 30,
  "includeOrderbook": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `markets` (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 = {
    "sources": [
        "polymarket",
        "kalshi"
    ],
    "status": "active",
    "tags": [],
    "seriesTickers": [],
    "maxMarketsPerSource": 30,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("romajure/prediction-market-odds").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 = {
    "sources": [
        "polymarket",
        "kalshi",
    ],
    "status": "active",
    "tags": [],
    "seriesTickers": [],
    "maxMarketsPerSource": 30,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("romajure/prediction-market-odds").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 '{
  "sources": [
    "polymarket",
    "kalshi"
  ],
  "status": "active",
  "tags": [],
  "seriesTickers": [],
  "maxMarketsPerSource": 30,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call romajure/prediction-market-odds --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,romajure/prediction-market-odds"
        }
    }
}
```

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/wfyJWDObc8wRF2Hl7/builds/1bLHBFtAi3Dv1Eea2/openapi.json
