Event Contract Odds - Kalshi API Scraper, Order Book, Trades
Pricing
from $0.50 / 1,000 result rows
Event Contract Odds - Kalshi API Scraper, Order Book, Trades
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.
Pricing
from $0.50 / 1,000 result rows
Rating
0.0
(0)
Developer
Snow Leo Data
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
23 days ago
Last modified
Categories
Share
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.9and up for near-certainties,0.1and below for longshots. - Closing within hours — markets resolving soon.
- Market status —
open,closed,settled,unopened. Settled markets carry theresult, which is what you need for backtesting.
What does the output look like?
A market row, abridged:
{"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:
- Set Series tickers to the series you follow, e.g.
KXFED. - Set Min open interest to a level that removes dead contracts, e.g.
100. - Turn on Monitor mode, set Min probability move to
0.05, give the schedule its own State key. - 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
liquidityfield is 0 for most markets in the feed, which is how the exchange publishes it. UseminLiquidityknowing it filters hard, or filter on open interest instead. turnover24hUsdis 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,
apiUrlin 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 againsttools/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.