# Event Contract Odds - Kalshi API Scraper, Order Book, Trades (`snow_leo_data/kalshi-scraper`) Actor

135,440 open Kalshi markets across 15,625 events in 77.3 seconds; the flat list is 99.1% parlay shards. Up to 99 fields. Prediction market data on event contracts: implied probability, spread, Kalshi trades and OHLC candles. Monitor mode emits only markets whose odds moved.

- **URL**: https://apify.com/snow_leo_data/kalshi-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

## Kalshi Prediction Market Scraper — Odds, Order Book & Event Contracts

Kalshi is a CFTC-regulated exchange where event contracts settle at $1 or $0, so
the price of a contract is the market's probability for that outcome. This Actor
turns that exchange into a dataset: one row per market, event or trade, with the
implied probability, the full quote, the order book, the recent tape and the
event metadata already joined together.

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 exchange is live, so counts move day to day.

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

One run gives you any of three shapes:

- **markets** — one row per tradable contract: quotes, implied probability,
  spread, volume, open interest, settlement rules, the event it belongs to and
  optionally the order book, the recent trades and OHLC candles. Up to 99 fields.
- **events** — one row per event, its markets nested inside, with the event
  priced as a whole: sum of yes asks, overround, and the gap below $1 when a
  mutually exclusive set is cheap. 27 fields.
- **trades** — the exchange-wide trade tape: every print with price, size,
  notional value and taker side. Measured: 5,000 trades in 5 requests and
  3.4 seconds.

#### Why does this Actor walk events instead of the market list?

Kalshi exposes the same market through two feeds, and they are not equivalent.

The flat `/markets` feed is the obvious one, and it is mostly noise. Measured on
2026-09-12: after 120 requests and 128.3 seconds it had returned 120,000 open
markets, **99.1% of them auto-generated multi-leg combination shards**
(`KXMVECROSSCATEGORY-...`), and it had not reached the end of the cursor. Those
shards are machine-generated parlays that almost never trade. A scraper built on
that feed spends most of its requests, and most of your patience, on rows no
human asked for.

The `/events` feed with nested markets returns the real book. Measured the same
day: **135,440 open markets across 15,625 events in 79 requests and 77.3
seconds, with zero combination shards in the result**. It also carries what the
flat feed does not have at all: the event title, the event subtitle, the
category, whether the outcomes are mutually exclusive, and the settlement
sources. That is why this Actor walks events — coverage, speed and joined
context come from the same decision.

The practical consequence for you: a full sweep of everything tradable on Kalshi
is about a minute of API time, not an hour, and you never pay for a row that is
a parlay shard.

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

The exchange returns prices and sizes as strings (`"0.4400"`, `"10293.97"`),
gives no probability, no spread, no depth totals and no time-to-close. This
Actor converts every money and size field to a number — an empty number is
`null`, never `""`, because an empty string in a float column breaks Excel,
BigQuery and pandas — and then computes:

| Field | What it is |
|---|---|
| `impliedProbability` | Mid of yes bid and ask. Live quotes, not a stale trade. |
| `impliedProbabilityLast` | Probability implied by the last trade, for comparison. |
| `noImpliedProbability` | The complement, so you do not have to compute it. |
| `spread`, `spreadPct` | Absolute spread and spread as a share of mid. |
| `yesPayoutRatio`, `noPayoutRatio` | Return per dollar if that side resolves true. |
| `annualizedReturnYes` | That return annualized over the time left to close. Only computed when more than an hour remains. |
| `hoursToClose`, `daysToExpiration`, `closesWithin24h` | Time left, precomputed. |
| `volume24hShare` | Share of lifetime volume that traded in the last 24 hours. |
| `turnover24hUsd` | 24h volume valued at the last price. An estimate, and labelled as one. |
| `openInterestUsd` | Open interest valued at the contract's notional value. |
| `bookDepthYes`, `bookDepthNo`, `bookDepthUsd`, `bookImbalance` | Totals across the order book, and which side is heavier. |
| `lastTradeAt`, `minutesSinceLastTrade`, `recentVwapYes`, `takerYesShare` | What the recent tape says about who is buying. |
| `windowHigh`, `windowLow`, `windowChange` | Price range over the candle window. |
| `eventYesAskSum`, `eventOverround`, `eventArbitrageGap` | Event-level pricing, see below. |

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

When an event is mutually exclusive — one of these outcomes must happen, exactly
one — the yes asks across all of 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. `eventYesAskSum`, `eventOverround` and `eventArbitrageGap` report that
directly.

This number is only produced for events the exchange marks as mutually
exclusive. A ladder of thresholds ("above 2.75%", "above 3.00%", …) is *not*
mutually exclusive — its outcomes are nested, the asks sum to something like
$4.87, and reporting that as an arbitrage would be nonsense. The Actor leaves
those fields `null` there rather than printing a number that looks like an edge
and is not one.

Computing this needs every market of the event in hand at once, which is another
thing the events walk gives for free.

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

Set **Monitor mode** on 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 as `NEW`, `UPDATED` or `UNCHANGED`,
and unchanged markets are not written to the dataset, so you are not billed for
them. Each row also carries `probabilityChange`, the move since the last time
that market was delivered.

There is a second control that matters more here than on any static source:
**Min probability move**. Quotes twitch by a cent constantly, so a plain content
hash would call nearly every market "updated" on every run and the saving would
vanish. Set it to `0.05` and a market is re-emitted only after its implied
probability has moved five points. This is tested both ways: a 2-point move
produces zero rows, a 20-point move produces the full set again.

Use **State key** to keep several schedules apart. Two schedules with different
keys do not see each other's memory.

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

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

- **Keyword** — substring over the question, both outcome labels, the ticker and
  the event title.
- **Categories** — the exchange's own categories, measured on the live feed:
  Sports 9,142 events, Elections 3,321, Entertainment 753, Politics 637,
  Financials 610, Economics 526, Climate and Weather 209, Science and Technology
  139, Crypto 91, Companies 79. An unrecognised name is reported in the log
  rather than silently returning nothing.
- **Series tickers** — a server-side filter and the cheapest way to follow one
  topic: `KXFED`, `KXNFLGAME`, `KXHIGHNY`. Paste a kalshi.com link instead of a
  ticker and the ticker is taken out of the link.
- **Event tickers** — fetched directly, one request each.
- **Market tickers** — an exact list of contracts.
- **Min 24h volume, min lifetime volume, min open interest, min liquidity** —
  most contracts on any prediction market never trade; these cut the dead ones.
  Open interest is the better liveness signal: it means money is still at risk,
  not that a trade once happened.
- **Min / max yes ask** — price band. `0.9` and up for near-certainties, `0.1`
  and below for longshots.
- **Closing within hours** — markets resolving soon.
- **Market status** — `open`, `closed`, `settled`, `unopened`. Settled markets
  carry the `result`, which is what you need for backtesting.

#### What does the output look like?

A market row, abridged:

```json
{
  "ticker": "KXFED-26SEP-T2.75",
  "eventTicker": "KXFED-26SEP",
  "seriesTicker": "KXFED",
  "title": "Will the upper bound of the federal funds rate be above 2.75% following the Fed's Sep 16, 2026 meeting?",
  "yesSubTitle": "Above 2.75%",
  "eventTitle": "Fed funds rate after Sep 2026 meeting?",
  "category": "Economics",
  "mutuallyExclusive": false,
  "settlementSources": ["Federal Reserve Board of Governors"],
  "seriesTitle": "Fed funds rate",
  "contractTermsUrl": "https://assets.kalshi.com/contract_terms/FED.pdf",
  "status": "active",
  "yesBid": 0.99,
  "yesAsk": 1.0,
  "noBid": 0.0,
  "noAsk": 0.01,
  "lastPrice": 0.99,
  "midPrice": 0.995,
  "spread": 0.01,
  "impliedProbability": 0.995,
  "noImpliedProbability": 0.005,
  "volume": 11455.96,
  "volume24h": 0.0,
  "openInterest": 9838.61,
  "openInterestUsd": 9838.61,
  "hoursToClose": 118.6,
  "closesWithin24h": false,
  "closeTime": "2026-09-16T17:55:00Z",
  "rulesPrimary": "If the upper bound of the target federal funds rate published on the Federal Reserve's official website is greater than 2.75% ...",
  "bookLevelsYes": 10,
  "bookDepthYes": 120505.77,
  "bookDepthUsd": 118958.21,
  "bookImbalance": 1.0,
  "marketUrl": "https://kalshi.com/markets/kxfed",
  "apiUrl": "https://api.elections.kalshi.com/trade-api/v2/markets/KXFED-26SEP-T2.75",
  "scrapedAt": "2026-09-12T19:18:53Z"
}
```

A market row carries 74 fields without enrichment and 99 fields with the order
book, the tape, candles and series metadata switched on. Compact output reduces
it to the 16 fields a decision is actually 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 |
| Full open-market sweep, 135,440 rows | $67.72 |

Filters and monitor mode both cut rows before they are written, so both cut the
bill directly. A measured example: a run over the `KXFED` series with order
books delivered 87 rows and used 88 API requests; the repeat run five minutes
later delivered 0 rows and used 1 request, because nothing had moved past the
threshold. In that hour the monitoring cost was the run-start fee alone.

Speed, measured in the cloud on 1 GB of memory: 1,000 rows in 5.1 seconds,
100 rows in 3.9 seconds.

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

A working setup for hourly monitoring of one topic:

1. Set **Series tickers** to the series you follow, e.g. `KXFED`.
2. Set **Min open interest** to a level that removes dead contracts, e.g. `100`.
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 markets.

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 account-level data.** Positions, balances, orders and leaderboards need
  an authenticated Kalshi account. This Actor uses only the public trade API —
  no key, no proxy, no browser.
- **Order book, trades and candles cost one extra API request per returned
  market.** A 1,000-row run with all three on is 3,000 extra requests and takes
  noticeably longer. They are off by default for that reason.
- **The `liquidity` field is 0 for most markets in the feed**, which is how the
  exchange publishes it. Use `minLiquidity` knowing it filters hard, or filter on
  open interest instead.
- **`turnover24hUsd` is an estimate**, 24h contract volume valued at the last
  price. The exchange does not publish a dollar turnover figure.
- **No raw JSON blob per row.** Every field is parsed and typed instead; if you
  need the untouched record, `apiUrl` in each row is the exact exchange endpoint
  that produced it.
- **No switch for excluding auto-generated parlays** — the events feed this
  Actor walks does not contain them at all, so there is nothing to exclude.

#### How is this checked?

Three suites, all runnable from the project:

- `python3 tests/test_logic.py` — arithmetic, filters, memory and shaping on
  recorded exchange responses, no network.
- `python3 tests/test_live.py` — the live API: cursor paging really advances,
  the events feed really has no combination shards, types arrive as promised.
- `python3 tests/test_lifecycle.py` — a run broken in the middle, resumed,
  repeated; memory between runs; the spending limit.
- `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 Kalshi account or API key?

No. The Actor reads Kalshi's public trade API directly — no key, no login, no
proxy and no browser. That is also why it is fast and why it cannot return
anything account-specific.

#### How current is the data?

Each row is a snapshot taken during the run; `scrapedAt` records the moment. The
exchange serves the whole open book in about 77 seconds, so a full sweep is
internally consistent to roughly a minute. For tighter tracking, filter to one
series or category and run more often.

#### Can you get historical and settled markets?

Yes. Set **Market status** to `settled` for resolved contracts, which carry the
`result` field, or `closed` for markets that stopped trading but have not
settled. Price history comes from **Include price history**, which returns OHLC
candles at 1-minute, 1-hour or 1-day resolution for the lookback window you set.

#### How do you find the ticker for a topic?

Run once with a keyword, read `seriesTicker` off the rows that interest you,
then use that series ticker for later runs — it is a server-side filter and much
cheaper than sweeping. Alternatively, paste the kalshi.com URL of the market page
into **Series tickers**; the ticker is extracted from it.

#### Why is `impliedProbability` different from the last price?

`impliedProbability` is the mid of the live bid and ask. `impliedProbabilityLast`
is the last traded price. On a thin market the last trade can be hours or days
old while the quotes are current, so the two disagree — both are given so you can
choose which one your model should trust.

#### 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 do not get a half-billed run, and the run report says
`chargeLimitReached`.

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

Yes. **Compact output** returns 16 core fields instead of 99, and **Drop empty
fields** removes nulls and empty arrays. Together they cut the payload enough to
fit many markets into a context window.

#### Where does the run report go?

To the key-value store under `REPORT`: API requests spent, events and markets
walked, what each filter removed before billing, the new/updated/unchanged split
in monitor mode, and any failures the run swallowed to protect the data it had
already collected.

#### Is scraping Kalshi allowed?

The Actor reads only Kalshi's public API endpoints, the same ones the website
uses, and does not log in or bypass anything. You are responsible for complying
with Kalshi's terms of use and with the rules that apply to you. This Actor is
not affiliated with or endorsed by Kalshi.

# Actor input Schema

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

Case-insensitive substring matched against the market question, the yes/no outcome labels, the ticker and the event title. Leave empty to take the whole feed.

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

markets — one row per tradable contract (default). events — one row per event with its markets nested and the event priced as a whole. trades — the exchange-wide trade tape.

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

Stop after this many rows. 0 means no limit: a full open-market sweep is 135,440 rows and takes about 77.3 seconds of API time (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.

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

Exchange categories with the number of open events measured on the live feed 2026-09-12: Sports 9,142, Elections 3,321, Entertainment 753, Politics 637, Financials 610, Economics 526, Climate and Weather 209.

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

open covers everything currently tradable. closed and settled give the historical and settlement view, including the result of each contract.

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

Server-side filter, the cheapest way to follow one topic: KXFED (Fed funds rate), KXNFLGAME (NFL games), KXHIGHNY (New York daily high temperature). A kalshi.com link pasted here works too — the ticker is taken out of it.

## `eventTickers` (type: `array`):

Fetch named events directly, one request each, e.g. KXFED-27APR. Faster than sweeping when you already know what you want.

## `tickers` (type: `array`):

Keep only these contract tickers, e.g. KXFED-27APR-T4.25. In trade-tape mode this selects whose trades are returned.

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

Contracts traded in the last 24 hours. Set 1 to keep only markets that traded at all today.

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

Total contracts traded since the market opened.

## `minOpenInterest` (type: `integer`):

Contracts still outstanding. A better liveness signal than volume: it means money is at risk right now, not that a trade once happened.

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

Dollars resting in the book as reported by the exchange feed. Many markets report 0 here, so any non-zero value filters hard.

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

Lower bound on the yes ask in dollars, 0 to 1. Use 0.9 for near-certain outcomes.

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

Upper bound on the yes ask in dollars, 0 to 1. Use 0.1 to list longshots only.

## `closingWithinHours` (type: `integer`):

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

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

Resting bids on both sides plus computed depth in contracts and dollars, and the yes/no imbalance. Costs one extra API request per returned market.

## `orderBookDepth` (type: `integer`):

Price levels per side.

## `includeTrades` (type: `boolean`):

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

## `tradesPerMarket` (type: `integer`):

How many recent trades to fetch per market.

## `includeCandles` (type: `boolean`):

OHLC candles for the lookback window, plus the window high, low and net change.

## `candleLookbackHours` (type: `integer`):

How far back the candle window reaches.

## `candlePeriodMinutes` (type: `string`):

Minutes per candle. The exchange serves 1, 60 and 1440.

## `includeSeries` (type: `boolean`):

Series title, publishing frequency, tags, fee type and the contract-terms PDF. The dictionary is cached between runs, so the second run asks for nothing.

## `tradeTapeHours` (type: `string`):

Used in trade-tape mode only: how far back the tape is read, in hours.

## `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 the implied probability 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`):

16 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 markets 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
{
  "query": "fed",
  "resultType": "markets",
  "maxItems": 100,
  "categories": [],
  "status": "open",
  "seriesTickers": [],
  "eventTickers": [],
  "tickers": [],
  "includeOrderBook": false,
  "orderBookDepth": 10,
  "includeTrades": false,
  "tradesPerMarket": 20,
  "includeCandles": false,
  "candleLookbackHours": 24,
  "candlePeriodMinutes": "60",
  "includeSeries": false,
  "tradeTapeHours": "1",
  "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 = {
    "query": "fed"
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/kalshi-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/EzjXfh823VJaEJAhk/builds/oiTcqDohGGeVCFRvQ/openapi.json
