# Prediction Markets Scraper (Kalshi + Polymarket) (`signalclear/prediction-markets-scraper`) Actor

Kalshi and Polymarket odds in one run: prices, implied probability, volume, open interest, order book and daily history, plus matched cross-venue pairs with the price spread. Public market data, no login. Bounded retries; failed venues are never charged.

- **URL**: https://apify.com/signalclear/prediction-markets-scraper.md
- **Developed by:** [signal clear](https://apify.com/signalclear) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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 Markets Scraper (Kalshi + Polymarket)

Get a snapshot of **Kalshi** and **Polymarket** in one run: each market's prices, implied probability, bid and ask, volume, open interest or liquidity, open and close times, and the outcome once it resolves. You can also add the order book and daily price history.

When both venues list the same question, the Actor adds a **pair row** with both Yes prices and the **spread** between them. NFL games, Super Bowl futures, NBA games and presidential nominations are the usual pairs.

- Both venues in one dataset, normalized to one schema (prices from 0 to 1).
- Cross-venue pairs with the spread, sorted from widest to narrowest.
- Bounded: every request has a timeout, every source has an attempt cap, and the market count per venue is capped.
- A venue or keyword that fails is never charged. Its rows are dropped, and the failure is recorded in the `FAILED_SOURCES` record.
- Public market data only, from the venues' own public APIs. No login, no API key, no account data.

### Input

| Field | Default | What it does |
|---|---|---|
| `venues` | `both` | `both`, `kalshi` or `polymarket`. Pairs need `both`. |
| `query` | empty | One keyword per line, such as `nfl`, `super bowl`, `fed rate` or `bitcoin`. Each line runs as its own source on each venue. Leave it empty to get the most-traded markets. |
| `category` | empty | `sports`, `politics`, `elections`, `economics`, `finance`, `weather`, `climate`, `crypto`, `entertainment`, `tech`, `science`, `business`, `world`, `health`, `commodities` or `ai`. Any other value is tried as a Polymarket tag, such as `nba`. Kalshi doesn't know those tags, so the Kalshi part fails and isn't charged. |
| `status` | `open` | `open`, `closed` (includes resolved markets) or `all`. |
| `minVolume` | 0 | Skips markets whose lifetime volume is lower. Kalshi counts contracts and Polymarket counts US dollars. |
| `matchAcrossVenues` | true | Adds the `type: pair` rows. |
| `includeOrderbook` | false | Adds the top 5 levels on each side, total depth and the spread. Costs one extra request per market. |
| `includeHistory` / `historyDays` | false / 30 | Adds one Yes price per day for up to 90 days. Costs one extra request per market. |
| `maxMarketsPerVenue` | 500 | The cap for each venue and each keyword. |
| `maxItems` | none | A hard cap on rows for the whole run, markets and pairs together. Pairs are written last. |
| `maxConcurrency` | 4 | The number of requests in flight to each venue at once. |
| `maxAttempts` | 3 (max 5) | Each attempt uses a fresh proxy session and reuses what earlier attempts already fetched. |
| `requestTimeoutSecs` | 30 | The timeout for each request. |
| `proxyConfiguration` | Apify proxy | Datacenter is enough. Residential isn't needed. |

Example:

```json
{
  "venues": "both",
  "query": ["nfl", "super bowl"],
  "category": "sports",
  "maxMarketsPerVenue": 200,
  "matchAcrossVenues": true
}
```

### Output

There are two row types. The dataset has a **Markets** view and a **Cross-venue pairs** view.

#### Market row (`type: market`)

```json
{
  "type": "market",
  "venue": "kalshi",
  "marketId": "KXNFLGAME-26OCT29CARGB-GB",
  "ticker": "KXNFLGAME-26OCT29CARGB-GB",
  "eventId": "KXNFLGAME-26OCT29CARGB",
  "seriesTicker": "KXNFLGAME",
  "title": "Green Bay wins",
  "eventTitle": "Carolina vs Green Bay",
  "subtitle": "Green Bay",
  "rulesSummary": "If Green Bay wins the Carolina vs Green Bay Pro Football game originally scheduled for Oct 29, 2026, then the market resolves to Yes.",
  "category": "Sports",
  "tags": ["Pro Football"],
  "status": "open",
  "outcomes": [
    {"name": "Yes", "price": 0.645, "impliedProbability": 0.645},
    {"name": "No", "price": 0.355, "impliedProbability": 0.355}
  ],
  "yesPrice": 0.645,
  "noPrice": 0.355,
  "lastPrice": 0.65,
  "bestBid": 0.64,
  "bestAsk": 0.65,
  "volume": 300707.87,
  "volume24h": 112288.41,
  "volumeUnit": "contracts",
  "openInterest": 264523.06,
  "openTime": "2026-10-20T04:06:00Z",
  "closeTime": "2026-11-01T00:15:00Z",
  "resolutionTime": "2026-10-30T03:15:00Z",
  "resolvedOutcome": null,
  "url": "https://kalshi.com/markets/kxnflgame",
  "query": "nfl",
  "scrapedAt": "2026-10-28T20:25:46Z"
}
```

Polymarket rows have the same fields. `slug` replaces `ticker`, `outcomePrices` lists every outcome's price, `liquidity` is in US dollars and `volumeUnit` is `usd`. Markets that aren't Yes/No, such as "Panthers vs. Packers" or an over/under, have `yesPrice: null` and carry their prices in `outcomes`.

How prices are set:

- **Polymarket:** `yesPrice` is the displayed outcome price, the same one the venue shows.
- **Kalshi:** `yesPrice` is the bid/ask midpoint when the book is tight (10 cents or less). Otherwise it is the last trade.
- **`impliedProbability`:** each outcome's price divided by the sum of all outcome prices.
- **Order book on:** `orderbook` holds `bids`, `asks`, `bidDepth`, `askDepth` and `spread`.
- **History on:** `priceHistory` holds `[{date, price}]`. Kalshi also gives the day's closing bid, ask, volume and open interest.

#### Pair row (`type: pair`)

```json
{
  "type": "pair",
  "title": "Browns vs. Steelers - Browns",
  "kalshiTitle": "Cleveland wins",
  "polymarketTitle": "Browns vs. Steelers",
  "polymarketOutcome": "Browns",
  "kalshiTicker": "KXNFLGAME-26NOV01CLEPIT-CLE",
  "polymarketId": "5603316",
  "kalshiYes": 0.365,
  "polymarketYes": 0.355,
  "spread": -0.01,
  "absSpread": 0.01,
  "matchMethod": "game",
  "matchScore": 1.0,
  "closeTimes": {"kalshi": "2026-11-01T21:00:00Z", "polymarket": "2026-11-01T18:00:00Z"},
  "kalshiUrl": "https://kalshi.com/markets/kxnflgame",
  "polymarketUrl": "https://polymarket.com/event/nfl-cle-pit-2026-11-01",
  "scrapedAt": "2026-10-28T20:25:46Z"
}
```

`spread` is the Polymarket Yes price minus the Kalshi Yes price. A positive spread means Polymarket prices the outcome higher.

### How pairing works, and its limits

Pairs are built only from markets saved in the same run, so both venues must return the markets in question. The Actor uses two methods.

1. **Games (`matchMethod: game`).** The Actor reads league, date and team codes from the Kalshi ticker (`KXNFLGAME-26NOV01CLEPIT-CLE`) and from the Polymarket moneyline slug (`nfl-cle-pit-2026-11-01`). The dates may differ by one day, because Kalshi dates games in US Eastern time and Polymarket uses UTC. Each team's Kalshi Yes price is compared with that team's Polymarket outcome price. Spreads, totals and player props are not paired.
2. **Everything else (`matchMethod: title`).** The Actor tokenizes both titles and weights the tokens by rarity, so a team or candidate name counts more than "champion". A candidate pair must:

   - agree on years and thresholds (35+ never pairs with 40+);
   - agree on Kalshi's city initials ("Los Angeles C" pairs with the Chargers, not the Rams);
   - resolve within 3 days of each other, or within half the time left until the earlier close for long-dated futures;
   - score at least 0.6.

   Pairs are then picked one-to-one, best score first.

Limits:

- Matching is fuzzy. Two markets can look alike and still settle on different rules or sources, or at different times. "Win the championship" and "win the Super Bowl" usually mean the same thing, but not always. **Read both markets' rules before acting on a spread.** This is a research tool and gives no trading signal.
- Prices are a snapshot. Thin books move, and a wide spread on a low-volume market is often just stale.
- Kalshi has no search and no volume sort. The Actor visits Kalshi's most-traded series first (by lifetime volume) and matches your keyword against series and event titles, tickers and tags. A small `maxMarketsPerVenue` can stop before it reaches the markets that Polymarket returned. Use the same keyword and category on both venues, and raise the cap if pairs are missing.

### Pricing

You pay per result: each market row and each pair row counts once. A venue or keyword that fails after every attempt writes no rows, so it costs nothing beyond the run's compute. It is listed in `FAILED_SOURCES` as `{venue, query, category, error, attempts}`. The run fails (exit 1) only when every source failed. Otherwise the status line reads, for example, `200 markets (+16 pairs) from 2 venues saved, 0 failed (not charged).`

Each source needs only a few requests. Kalshi's series catalog is the largest download, fetched once per run: about 0.3 MB compressed for Sports and about 1.8 MB without one. A keyword works without a category; setting one narrows the results and makes the run a little faster. The order book and history each add one request per market.

### Legal

This Actor reads public market data from Kalshi's and Polymarket's public, unauthenticated APIs. It uses no login, no account, no API key and no personal data. It keeps its requests under the venues' published rate limits and backs off on HTTP 429. Your use of the data is governed by each venue's terms. **This is not financial, investment or betting advice.** Prediction-market trading is restricted in some places. Check what applies to you.

# Actor input Schema

## `venues` (type: `string`):

Which prediction markets to read. Cross-venue pairs need both.

## `query` (type: `array`):

One keyword or phrase per line, such as nfl, super bowl, fed rate or bitcoin. Each line is searched separately on each venue and fails or succeeds on its own. Kalshi matches every word against series and event titles, tickers and tags; Polymarket uses its own search, or lists the tag when the keyword is a tag (nfl, nba). Leave empty for the most-traded markets.

## `category` (type: `string`):

Optional. One of sports, politics, elections, economics, finance, weather, climate, crypto, entertainment, tech, science, business, world, health, commodities, ai. Any other value is tried as a Polymarket tag (for example nfl or nba) and is unknown to Kalshi, so the Kalshi part fails (and is not charged). Optional with a keyword too: without a category, Kalshi's whole series catalog is searched.

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

Open markets (trading now), closed markets (trading ended, including resolved ones with their outcome) or all.

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

Skip markets with lower lifetime volume. Units differ by venue: contracts on Kalshi (each pays $1), US dollars on Polymarket. 0 keeps everything.

## `matchAcrossVenues` (type: `boolean`):

Adds a pair row (type: pair) for each question found on both venues, with both Yes prices and the spread. Game winners are matched by league, teams and date; other markets by fuzzy title and resolution date. Matching is approximate: check both markets' rules before acting on a spread.

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

Adds the top 5 bid and ask levels, total depth and the bid-ask spread for the Yes side. One extra request per market.

## `includeHistory` (type: `boolean`):

Adds one Yes price per day for the last N days (see History days). One extra request per market.

## `historyDays` (type: `integer`):

How many days of daily price history to keep when price history is on.

## `maxMarketsPerVenue` (type: `integer`):

Stop after this many markets from each venue for each keyword. Kalshi markets come from its most-traded series first; Polymarket markets by 24-hour volume.

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

Hard cap on dataset rows for the whole run, markets and pairs together. Pairs are added last, so a low cap can leave no room for them. Leave empty for no cap.

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

Requests in flight to each venue at once. Requests are also paced to stay under the venues' public rate limits and slow down further on HTTP 429.

## `maxAttempts` (type: `integer`):

Each attempt uses a fresh proxy session and replays what the previous one already fetched. A venue/keyword that still fails is listed in the FAILED_SOURCES record and is not charged.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one HTTP response before the attempt counts as failed and is retried on a fresh session.

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

Both venues' public APIs answer datacenter IPs, so the default Apify proxy is enough. Residential is not needed.

## Actor input object example

```json
{
  "venues": "both",
  "query": [
    "nfl"
  ],
  "status": "open",
  "minVolume": 0,
  "matchAcrossVenues": true,
  "includeOrderbook": false,
  "includeHistory": false,
  "historyDays": 30,
  "maxMarketsPerVenue": 500,
  "maxConcurrency": 4,
  "maxAttempts": 3,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `markets` (type: `string`):

Dataset results in the "Markets" view.

## `pairs` (type: `string`):

Dataset results in the "Cross-venue pairs" view.

## `failed` (type: `string`):

JSON list of venue queries that failed after every retry, with the error for each. Empty when everything succeeded. Failed inputs are never charged.

# 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 = {
    "query": [
        "nfl"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("signalclear/prediction-markets-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 = {
    "query": ["nfl"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("signalclear/prediction-markets-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 '{
  "query": [
    "nfl"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call signalclear/prediction-markets-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,signalclear/prediction-markets-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/TIgTlEa1kKGdyXJ1b/builds/m7P2Hqj51wWe13Dqg/openapi.json
