# Prediction Markets Data: Kalshi + Polymarket in One Schema (`magenta_courser/prediction-markets`) Actor

Pick Kalshi, Polymarket or both, optionally filter by keyword or category, and get one flat row per outcome: probability (0-1), bid/ask, 1h/24h/7d change, volume, liquidity, close time and URL. Optional 7-day price history and changed-since-last-run mode. Official public APIs.

- **URL**: https://apify.com/magenta_courser/prediction-markets.md
- **Developed by:** [SUNGHWAN CHO](https://apify.com/magenta_courser) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 market outcomes

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

## Prediction Markets Data: Kalshi + Polymarket in One Schema

Choose Kalshi, Polymarket or both (optionally a keyword, category, status or minimum volume), and get **one flat row per market outcome** with the implied probability (0–1), best bid/ask, 1h/24h/7d price change, volume, liquidity or open interest, close time and a link to the market.

- Both venues in **the same English field names**, numbers as numbers, missing values as `null`.
- Open markets come **highest 24-hour volume first**.
- Optional **7-day hourly price history** per outcome.
- Optional **"only what changed since my last run"** mode for scheduled monitoring and alerts.
- Uses only the venues' **official public, unauthenticated read APIs**. No login, no browser, no proxy.

### What you get

One row per outcome:

- A single yes/no question (e.g. *"Will the Fed cut rates in December?"*) → one row, `outcome: "Yes"`.
- A multi-outcome event (e.g. *"Who will win the 2028 Democratic nomination?"*) → one row per candidate, `outcome: "Gavin Newsom"`, …
- A two-sided Polymarket market (e.g. a game *"Ravens vs. Falcons"*) → one row per side.

| Field | Meaning |
|---|---|
| `venue` | `polymarket` or `kalshi` |
| `eventId`, `eventTitle` | Event the market belongs to |
| `marketId`, `question` | Kalshi market ticker / Polymarket condition id, and the market question |
| `outcome`, `outcomeId` | What this row prices (candidate, team, range or `Yes`); Kalshi ticker / Polymarket CLOB token id |
| `probability` | Implied probability 0–1 (see below) |
| `lastPrice`, `bestBid`, `bestAsk`, `spread` | Prices on the 0–1 scale |
| `change1h`, `change24h`, `change7d` | Price change on the 0–1 scale (0.05 = +5 points) |
| `volume24h`, `volumeTotal`, `volumeUnit` | Volume; `volumeUnit` is `USD` (Polymarket) or `contracts` (Kalshi, each contract pays $1) |
| `liquidity` | Polymarket order-book liquidity in USD (`null` on Kalshi) |
| `openInterest` | Kalshi open interest in contracts (`null` on Polymarket) |
| `closeTime` | Scheduled close, ISO 8601 |
| `status`, `resolvedOutcome` | `open`, `closed`, `resolved`, `upcoming`; the winning outcome once resolved |
| `category`, `tags` | Kalshi event category; Polymarket main category and all tags |
| `url` | Market page |
| `previousProbability`, `probabilityChangeSinceLastRun`, `changeType` | Filled in change mode only |
| `priceHistory` | `[{ "time", "price" }]` hourly for 7 days, when requested |
| `scrapedAt`, `schemaVersion` | Collection time and output version |

Example row:

```json
{
  "venue": "kalshi",
  "eventId": "KXNEXTNATOSECGEN-99",
  "eventTitle": "Who will be the next Secretary General of NATO?",
  "marketId": "KXNEXTNATOSECGEN-99-KKAL",
  "question": "Will Kaja Kallas be the next Secretary General of NATO?",
  "outcome": "Kaja Kallas",
  "outcomeId": "KXNEXTNATOSECGEN-99-KKAL",
  "probability": 0.21,
  "lastPrice": 0.21,
  "bestBid": 0.25,
  "bestAsk": 0.47,
  "spread": 0.22,
  "change1h": null,
  "change24h": 0.088,
  "change7d": null,
  "volume24h": 0,
  "volumeTotal": 1936.64,
  "volumeUnit": "contracts",
  "liquidity": null,
  "openInterest": 1174.5,
  "closeTime": "2099-01-08T15:00:00.000Z",
  "status": "open",
  "resolvedOutcome": null,
  "category": "Elections",
  "tags": ["Elections"],
  "url": "https://kalshi.com/markets/kxnextnatosecgen",
  "previousProbability": null,
  "probabilityChangeSinceLastRun": null,
  "changeType": null,
  "priceHistory": null,
  "scrapedAt": "2026-10-12T03:48:47.026Z",
  "schemaVersion": 1
}
```

#### How `probability` is computed

- **Polymarket**: the outcome price Polymarket itself displays (`outcomePrices`).
- **Kalshi**: the midpoint of best bid and best ask when the spread is 10 cents or less, otherwise the last traded price (the same rule Polymarket uses for its display). `null` if the market never traded and has no two-sided book.

#### Where the changes come from

- Polymarket: the 1h/24h/7d changes Polymarket publishes for each market; missing ones are filled from price history when you turn it on.
- Kalshi: `change24h` = last price − price 24 hours ago. `change1h` and `change7d` need **Include 7-day price history** (then all three are computed from the hourly history, against `probability`).

### Input

| Field | Default | Notes |
|---|---|---|
| `venues` | both | `polymarket`, `kalshi` |
| `keywords` | – | Any of these words in event title, question or outcome (case-insensitive) |
| `categories` | – | Whole-word match on category/tags, e.g. `Politics`, `Crypto`, `Economics`, `Weather` |
| `status` | `open` | `open`, `closed` (resolved first, most recent first), `all` (open first) |
| `maxItems` | 100 | Rows **per venue** (1–10,000) |
| `minVolume24h` | 0 | USD on Polymarket, contracts on Kalshi |
| `closingWithinDays` | – | Only markets closing within N days |
| `includePriceHistory` | false | Adds 7-day hourly `priceHistory` |
| `onlyChangedSinceLastRun` | false | Change mode, see below |
| `minProbabilityChange` | 0.01 | Smallest move that counts as a change (0–1 scale) |
| `historyStoreName` | `prediction-markets-history` | Named key-value store that remembers the last returned probabilities |

Example — top 50 crypto markets on both venues:

```json
{ "categories": ["Crypto"], "maxItems": 50 }
```

Example — every 15 minutes, alert on Fed / rates markets that moved 3+ points:

```json
{
  "keywords": ["fed", "rate cut", "fomc"],
  "onlyChangedSinceLastRun": true,
  "minProbabilityChange": 0.03,
  "historyStoreName": "fed-watch"
}
```

### Change mode

With `onlyChangedSinceLastRun`, the Actor builds the same list as usual (filters + `maxItems`) and then returns only outcomes that are **new**, **changed status**, or moved at least `minProbabilityChange` **since they were last returned**. The comparison point only moves when a row is returned, so slow drifts are still reported once they add up. Use a different `historyStoreName` for each watchlist. Two runs with the same store at the same moment may report the same change twice.

### Pricing

Pay per event: one **`market-outcome`** event per row saved to the dataset. Runs that save nothing (no matches, or nothing changed) cost no events; failed requests are never charged. If you set a **Maximum cost per run**, the Actor stops cleanly when it is reached and says so in the status message and in `RUN_SUMMARY`.

### Run summary

The key-value store record `RUN_SUMMARY` (free) has per-venue `status` (`completed`, `failed`, `budgetLimited`), `stopReason`, markets scanned, rows matched/saved, rows unchanged (change mode) and request counts. If one venue fails, the other venue's rows are still saved; the run fails only if every venue failed.

### Speed and limits

- **Polymarket** returns markets sorted by 24h volume, so the Actor stops as soon as it has enough rows. Up to 5,000 open markets are scanned per run when a filter is narrow.
- **Kalshi** does not sort by volume, so the Actor reads the whole open list (about 12,000 events / 120,000 markets, roughly 2–4 minutes at 256 MB) and sorts it. With `status: closed`, resolved (settled) markets come first, then closed-but-unresolved ones, most recently closed first; up to 2,000 events of each are scanned and Kalshi markets that never traded are skipped.
- `status: all` returns open markets first and fills the rest with closed ones.
- Requests are paced (about 2–3 per second) and well under both venues' published rate limits.
- The Actor must run on the Apify platform. Polymarket blocks some countries; this Actor does not try to get around regional restrictions.

### Notes and responsible use

- This Actor reads public market data through official, unauthenticated read endpoints. It does not log in, trade, or access any account or personal data.
- Kalshi's Developer Agreement and data terms and Polymarket's Terms of Use restrict how their data may be stored, redistributed and used commercially. **You are responsible for making sure your use of the data complies with those terms.** Not investment advice.
- Kalshi market URLs point to the series page.
- Cross-venue matching ("is this Kalshi market the same as that Polymarket market?") is deliberately not done: resolution rules and close times differ even when the questions look alike.

# Actor input Schema

## `venues` (type: `array`):

Which prediction markets to read. Leave both selected to get Kalshi and Polymarket in the same flat schema.

## `keywords` (type: `array`):

Keep only outcomes whose event title, question or outcome name contains any of these words (case-insensitive), e.g. fed, bitcoin, election. Leave empty for all markets.

## `categories` (type: `array`):

Keep only markets in these categories or tags, matched as whole words, e.g. Politics, Crypto, Sports, Economics, Weather. Kalshi uses its event category; Polymarket uses its tags.

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

open = currently trading (default). closed = no longer trading, including resolved markets (newest first). all = both.

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

Maximum outcome rows to return from each venue. Open markets are returned highest 24h volume first.

## `minVolume24h` (type: `number`):

Skip markets with less 24h volume than this. Polymarket volume is in USD; Kalshi volume is in contracts (each pays $1). 0 = no minimum.

## `closingWithinDays` (type: `integer`):

Keep only markets whose close time is within this many days from now. Leave empty for any close time.

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

Adds priceHistory (hourly points for the last 7 days) to each row and fills missing 1h/24h/7d changes. Makes runs slower (one extra request per Polymarket outcome, one per 50 Kalshi markets). Same price per row.

## `onlyChangedSinceLastRun` (type: `boolean`):

Return only outcomes that are new, changed status, or moved at least "Minimum probability change" since they were last returned. The memory is kept in the named key-value store below, so schedule the same input with the same store name.

## `minProbabilityChange` (type: `number`):

Used with "Only markets that changed": the smallest move that counts as a change, on the 0-1 scale (0.01 = 1 percentage point).

## `historyStoreName` (type: `string`):

Name of the key-value store that remembers the last returned probabilities (3-63 letters, digits or hyphens). Use a different name for each watchlist.

## Actor input object example

```json
{
  "venues": [
    "polymarket",
    "kalshi"
  ],
  "status": "open",
  "maxItems": 100,
  "minVolume24h": 0,
  "includePriceHistory": false,
  "onlyChangedSinceLastRun": false,
  "minProbabilityChange": 0.01,
  "historyStoreName": "prediction-markets-history"
}
```

# Actor output Schema

## `outcomes` (type: `string`):

One flat row per market outcome.

## `runSummary` (type: `string`):

Per-venue status, stop reason, markets scanned, rows saved and request counts. Not billed.

# 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 = {
    "venues": [
        "polymarket",
        "kalshi"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("magenta_courser/prediction-markets").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 = { "venues": [
        "polymarket",
        "kalshi",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("magenta_courser/prediction-markets").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 '{
  "venues": [
    "polymarket",
    "kalshi"
  ]
}' |
apify call magenta_courser/prediction-markets --silent --output-dataset

```

## MCP server setup

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

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/LGGbMnLwlNN8NiFQo/builds/vtFDTOwahKVKZkUDl/openapi.json
