Kalshi & Polymarket Prediction Market Odds Scraper
Live odds, volume, liquidity and settlement from Kalshi and Polymarket in one schema — with weather markets resolved to city, station and temperature bracket.
Verification
Output fields
Authoritative field set from the dataset schema.
| Field | Type |
|---|---|
venue | string|null |
market_id | string|null |
event_id | string|null |
series | string|null |
question | string|null |
subtitle | string|null |
category | string|null |
status | string|null |
outcomes | array|null |
outcome_prices | array|null |
yes_price | number|null |
no_price | number|null |
yes_bid | number|null |
yes_ask | number|null |
no_bid | number|null |
no_ask | number|null |
last_price | number|null |
implied_probability | number|null |
volume | number|null |
volume_24h | number|null |
open_interest | number|null |
liquidity | number|null |
open_time | string|null |
close_time | string|null |
result | string|null |
weather_city | string|null |
weather_station | string|null |
bracket_low | number|null |
bracket_high | number|null |
bracket_type | string|null |
market_url | string|null |
notice | string|null |
notice_type | string|null |
source_url | string|null |
scraped_at | string|null |
Sample row
{
"source_url": "https://api.elections.kalshi.com/trade-api/v2/markets/KXHIGHMIA-26SEP07-B86.5",
"venue": "kalshi",
"market_id": "KXHIGHMIA-26SEP07-B86.5",
"event_id": "KXHIGHMIA-26SEP07",
"series": "KXHIGHMIA",
"question": "Will the maximum temperature be 86-87° on Sep 7, 2026?",
"subtitle": "86° to 87°",
"category": "Climate and Weather",
"status": "active",
"outcomes": [
"Yes",
"No"
],
"outcome_prices": [
0.03,
0.98
],
"yes_price": 0.03,
"no_price": 0.98,
"yes_bid": 0.02,
"yes_ask": 0.03,
"no_bid": 0.97,
"no_ask": 0.98,
"last_price": 0.02,
"implied_probability": 0.02,
"volume": 6136.62,
"volume_24h": 6136.62,
"open_interest": 4570.47,
"liquidity": 0,
"open_time": "2026-09-06T14:00:00Z",
"close_time": "2026-09-08T05:00:00Z",
"result": null,
"weather_city": "Miami",
"weather_station": "CLIMIA",
"bracket_low": 86,
"bracket_high": 87,
"bracket_type": "between",
"market_url": null
} Inputs
| Field | Type | Req | Default | Description |
|---|---|---|---|---|
venue | string | no | "both" | Which exchange to read. 'Both' returns one combined, directly comparable dataset — the same question is often listed on each at a different price. |
query | string | no | "fed" | Keep only markets whose question, subtitle, series, ticker or weather city contains this text. Matching the city matters: a New York temperature market's question says only 'Will the maximum temperature be 81-82°?', so 'new york' is the only way to find it. Kept forever alongside 'Keywords' — saved tasks and API callers send it. |
queries | array | no | ["fed","bitcoin","hurricane"] | Several keywords in ONE run. A market is kept if it matches any of them. This does not re-fetch per keyword — the exchange is read once and filtered in memory, so ten keywords cost exactly what one does. |
categories | array | no | [] | Kalshi's own categories, e.g. Politics, Economics, Climate and Weather, Crypto, Sports, Financials. Filtered here rather than at the API, because Kalshi accepts a category parameter and then ignores it — /events?category=Crypto and ?category=Climate%20and%20Weather return byte-identical pages. Polymarket publishes no category per market, so this filter excludes its rows when set. Left empty by default: every filter here narrows, and pre-filling two of them together is how an Actor returns nothing on its first run. |
series | array | no | [] | Read only these Kalshi series, e.g. KXHIGHNY (New York daily high), KXHIGHCHI, KXHIGHMIA, KXFEDDECISION. This one IS honoured by the API, so naming a series turns a full-exchange walk into a single request — about 0.5s instead of 17s. Leave empty to read everything; a series narrows to that series alone, so combining it with keywords usually returns nothing. |
minVolume | integer | no | 0 | Drop markets that have traded less than this. Most of any prediction exchange is long-tail listings nobody has touched; set 1000 or more to get only markets with a real book behind the price. |
includeClosed | boolean | no | false | Off by default, so you get the live board. Turn it on to pull settled markets with their result, which is what you want for backtesting rather than trading. |
maxResults | integer | no | 1000 | A cap on the whole RUN, not per keyword — this Actor sweeps entire exchanges rather than looking up one thing at a time, so the cap is your spend control. Markets are ranked by volume before it applies, so a cap keeps the ones people are actually trading. The run log always states how many matched, so a cap can never read as completeness. |
proxyConfiguration | object | no | {"useApifyProxy":false} | Optional and off by default. Both venues are public, documented, keyless APIs that answer a plain client in under a second, and a proxy only adds latency (#145: never reach for one before the plain path is proven to fail). |
Pricing
Pay per result (market) — no charge for empty runs.
Example: 1000 markets at $0.0015/result ≈ $1.50.
FAQ
Why does this read Kalshi's events endpoint instead of its markets endpoint?
Because the markets endpoint cannot be used as a market list. On 2026-09-07, `GET /trade-api/v2/markets?limit=200&status=open` returned 25,000 rows of which 24,992 were auto-generated multivariate parlay legs — combination contracts with titles like 'yes Kaede Katayama,yes Eric Jr Olivarez,yes Aniketh Venkataraman' — leaving 8 real markets, every one of them with zero volume. Paging does not get past them; they fill the whole cursor. `GET /events?with_nested_markets=true` returns the same exchange with none of them: 11,206 events and 100,643 markets on the same day. You can check both in a browser; no key is needed. If a prediction-market dataset you already have is full of comma-separated player names, this is why.
What are the '_dollars' and '_fp' field names about?
Kalshi renamed its price and size fields and changed their type. `last_price` is now `last_price_dollars`, `volume` is `volume_fp`, `open_interest` is `open_interest_fp`, and the values are decimal strings ('0.9510') rather than integer cents. The previous names are gone, not deprecated — a request returns no such key at all. Anything written against the old names therefore reads null for every price while still producing rows of the right shape, which is why a broken feed in this category tends to look healthy rather than throw an error.
What happens if Kalshi renames the fields again?
The run returns a single free row with `notice_type: source_changed` saying so, and no market rows. That is deliberate: this category's characteristic failure is a silent rename, and a dataset of correctly-shaped rows with null prices is worse than no dataset, because nothing downstream can tell the difference. The check is across the whole fetch, not per row — an individual market legitimately has no bid.
Why is the category filter applied after fetching rather than by the API?
Because Kalshi's own category parameter does not work. `/events?status=open&category=Crypto` and `/events?status=open&category=Climate%20and%20Weather` return byte-identical pages — same events, same order. Passing it through would produce a filter that appears to work and silently does not. Series tickers are different: `series_ticker=` is honoured, and naming one turns a 17-second full walk into a single 0.5-second request.
What do the weather fields give me that the question does not?
The city and the station. A Kalshi temperature market's question is 'Will the maximum temperature be 86-87° on Sep 7, 2026?' — it never says Miami, and it never says CLIMIA, which is the observation station whose reading actually settles the contract. Both are stated only in the prose of the resolution rules, and both are what you need to line a bracket up against a forecast. The bracket bounds come through as numbers (`bracket_low`, `bracket_high`, `bracket_type`) rather than as text you would have to parse out of a title.
Why is implied_probability sometimes null rather than 0?
Because the market has not said anything. A contract that settles at $1 has a price that is also its probability, so where there is a last trade, or a book to take a mid from, that number is the market's own estimate. Where a market has never traded and has no resting orders, there is no estimate — and 0 would assert that the market considers the outcome impossible, which is a much stronger claim than silence. Filtering for long shots gives the same answer either way; the difference shows when you average, where a wall of false zeros would drag the mean down.
Why do Kalshi rows have no market_url?
Because Kalshi's public market page is at kalshi.com/markets/<series>/<slug>, and its API never states that slug. The URL could be assembled from the parts, but an assembled link is a claim on every single row that nothing has verified, and a wrong one 404s for the buyer. `source_url` on every row is the venue's own per-market API endpoint, which was verified to answer 200, so the row is auditable either way. Polymarket does publish its slug, so its rows carry a real page link.
How many markets can one run return?
As many as you ask for. On 2026-09-07 there were 100,643 open Kalshi markets and about 2,100 reachable active Polymarket markets, and a full sweep of both took roughly 20 seconds. 'Maximum markets per run' caps the whole run rather than each keyword, because this Actor sweeps exchanges rather than looking things up one at a time — so the cap is your spend control. Markets are sorted by volume before the cap applies, and the run log always states how many matched, so a cap can never read as completeness.
Why can't I get all of Polymarket?
Polymarket refuses paging past offset 2000 with 'offset too large', and caps a page at 100 rows however much you request, so about 2,100 active markets are reachable per sweep. When that ceiling is hit the run adds a free `truncated` row saying so rather than presenting a partial sweep as the whole exchange. Narrowing with a keyword reaches markets beyond it.
Do I need a proxy or an account on either venue?
No. Both APIs are public, documented and keyless, and they answer a plain client in well under a second, so the proxy input is off by default and turning it on only adds latency. No Kalshi or Polymarket account is involved and nothing here is authenticated.
Is any of this data inferred or modelled?
No. Every field is read from the venue's own response. Implied probability is arithmetic on the venue's own last trade or book, the weather city and station are quoted out of the venue's own resolution rules, and the bracket bounds are the venue's own numeric strike fields. Nothing is estimated by a model, and no field is filled in where the venue said nothing.
Guides & use cases
Related actors