Prediction Market Odds API — Polymarket + Kalshi
Pricing
from $0.01 / 1,000 results
Prediction Market Odds API — Polymarket + Kalshi
Search or look up Polymarket and Kalshi prediction markets and get one unified JSON schema back: prices, volume, liquidity, order books, and de-vigged fair probabilities (multiplicative/power method). No API key needed — built for research, arbitrage-scanning, and AI agents.
Pricing
from $0.01 / 1,000 results
Rating
0.0
(0)
Developer
Daniel Posztos
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
17 days ago
Last modified
Categories
Share
Prediction Market Odds API — Polymarket + Kalshi, De-vigged Fair Probabilities
SEO title: Prediction Market Odds API — Polymarket & Kalshi Scraper, extract de-vigged probabilities as JSON SEO description: Search or look up Polymarket and Kalshi prediction markets and get one unified JSON schema back: prices, volume, liquidity, order books, and de-vigged fair probabilities (multiplicative/power method). No API key needed — built for research, arbitrage-scanning, and AI agents.
One actor, one schema, two prediction market platforms. Query by keyword or by exact market ID, get back current prices AND the de-vigged "fair" probability for every outcome — the overround-adjusted number you actually want if you're comparing markets, building a model, or looking for mispriced longshots.
Use with AI agents (MCP / LangChain)
Built to be called as a tool by an AI agent, not just from the Console. The tool definition (name, description, arguments) is generated automatically from this actor's title and input schema, so an LLM can pick it and fill the arguments correctly.
MCP (Claude Desktop / Cursor / VS Code) — add to your MCP client config:
{"mcpServers": {"apify": {"url": "https://mcp.apify.com","headers": { "Authorization": "Bearer <APIFY_TOKEN>" },"actors": ["westerly_breaker/prediction-market-odds-api"]}}}
LangChain:
from langchain_apify import ApifyActorsToolodds = ApifyActorsTool("westerly_breaker/prediction-market-odds-api") # APIFY_TOKEN from envresult = odds.invoke({"query": "bitcoin", "max_markets": 5})
Why agents like it: no API key needed, one unified schema across Polymarket and Kalshi, and
de-vigged fair probabilities computed for you — exactly the shape a trading/research agent wants. Set
include_orderbook: false (default) to keep responses small and cheap; turn it on only when depth matters.
Standby (low-latency HTTP API, no cold start) — Standby mode is enabled for this actor, so you can
skip the batch run/dataset round-trip entirely and call it as a plain HTTP API. Authenticate with your
own Apify token (Authorization: Bearer <APIFY_TOKEN>, or ?token=<APIFY_TOKEN>):
curl -H "Authorization: Bearer <APIFY_TOKEN>" \"https://westerly-breaker--prediction-market-odds-api.apify.actor/search?query=bitcoin&max_markets=5&include_orderbook=false"
or POST the same fields as a JSON body (identical shape to the Console/API input):
curl -X POST "https://westerly-breaker--prediction-market-odds-api.apify.actor/search" \-H "Authorization: Bearer <APIFY_TOKEN>" \-H "Content-Type: application/json" \-d '{"query": "bitcoin", "max_markets": 5, "include_orderbook": false}'
Both return {"query": ..., "count": ..., "items": [...]} directly in the HTTP response — no dataset,
no Actor.push_data. GET / is a free health check ({"status": "ready", ...}, never charged). Query
parameters are plain strings, coerced to the right type server-side (max_markets=5 → 5,
include_orderbook=true → true, platforms=polymarket,kalshi or repeated platforms= params → a
list); invalid input returns HTTP 400 with a speaking error message rather than failing a run. Each
returned market still costs exactly one market-result charge, same as the batch flow — a
budget-limited caller gets back fewer, fully-paid markets rather than a response it wasn't billed for
the tail of. When include_orderbook is true, an order book is only attached to a market once its own
orderbook-snapshot charge succeeds: on a tight budget you may get back a fully-paid market with no
orderbook field rather than an unpaid one, but a market is never dropped just because its order book
couldn't be paid for. The Standby endpoint above is live.
Recipes
Copy-paste starting points — each is a single call that returns finished, de-vigged JSON.
Scan for cross-platform mispricing (Polymarket vs Kalshi on the same theme). Ask both platforms for
the same keyword, then compare devigged_prob for equivalent outcomes — a gap wider than both platforms'
combined fees is a candidate:
curl -H "Authorization: Bearer <APIFY_TOKEN>" \"https://westerly-breaker--prediction-market-odds-api.apify.actor/search?query=bitcoin&platforms=polymarket,kalshi&max_markets=40"
Give a trading/research agent a fair-probability feed. The de-vig is already done, so an LLM tool call
gets the number it should reason about (devigged_prob), not the vig-inflated headline price. Keep
responses small and cheap by leaving include_orderbook off until depth actually matters:
from langchain_apify import ApifyActorsToolodds = ApifyActorsTool("westerly_breaker/prediction-market-odds-api")fair = odds.invoke({"query": "2028 election", "max_markets": 10}) # devigged_prob per outcome
Pull one specific market by ID for monitoring. Skip search entirely and poll exact markets (Polymarket condition IDs / slugs, Kalshi tickers) on the low-latency Standby endpoint:
curl -X POST "https://westerly-breaker--prediction-market-odds-api.apify.actor/search" \-H "Authorization: Bearer <APIFY_TOKEN>" -H "Content-Type: application/json" \-d '{"market_ids": ["KXBTCD-26JUL0517-T52999.99"], "include_orderbook": true}'
Why this exists
Polymarket (Gamma + CLOB APIs) and Kalshi (trade-api v2) both expose public, unauthenticated market data — but in two different shapes, with prices that still include the platform's own vig/overround. This actor:
- queries both (or either) platform for markets matching a keyword, or fetches specific markets by ID,
- normalizes them into one schema,
- removes the vig with a standard de-vig method (multiplicative by default) so
devigged_probvalues for a market's outcomes actually sum to 1.0, - optionally attaches a top-of-book order book snapshot.
No scraping, no anti-bot risk, no login — both platforms' public market-data endpoints are called directly (over HTTPS, no browser).
Input
| Field | Type | Default | Description |
|---|---|---|---|
query | string | — | Free-text search keyword, e.g. "bitcoin", "2028 election". Use this OR market_ids. |
market_ids | array of strings | [] | Exact market IDs to fetch directly: Polymarket condition IDs (0x...) or slugs, and/or Kalshi tickers (e.g. "KXBTCD-26JUL0517-T52999.99"). Mixing IDs from both platforms in one list is fine — each platform only matches its own IDs. |
platforms | array of strings | ["polymarket", "kalshi"] | Which platform(s) to query. |
include_orderbook | boolean | false | Attach a top-10-level order book per market. Bills an extra event (see pricing) and roughly doubles request count. |
max_markets | integer (1–200) | 20 | Total markets to return across all platforms combined. Split evenly between the requested platforms so one platform's abundance of matches (Polymarket tends to have far more markets matching a broad query than Kalshi) doesn't crowd out the other. |
You must provide query and/or market_ids — an actor input with neither fails immediately with a message telling you exactly that.
Worked example — input
{"query": "bitcoin","platforms": ["polymarket", "kalshi"],"include_orderbook": false,"max_markets": 5}
Worked example — output (2 of 5 items, real data)
[{"platform": "polymarket","market_id": "0x4863841fee98ae432b657dbad973cd5562e7fa6bab5e1a725ebd833723c9493d","question": "Will the price of Bitcoin be above $50,000 on July 5?","outcomes": [{ "name": "Yes", "price": 0.9995, "implied_prob": 0.9995, "devigged_prob": 0.9995 },{ "name": "No", "price": 0.0005, "implied_prob": 0.0005, "devigged_prob": 0.0005 }],"volume": 132828.07,"liquidity": 126913.72,"close_time": "2026-07-05T16:00:00Z","url": "https://polymarket.com/event/bitcoin-above-50k-on-july-5-2026","scraped_at": "2026-07-05T14:57:29.520Z"},{"platform": "kalshi","market_id": "KXBTCMAX150-25-26OCT31-149999.99","question": "When will Bitcoin cross $100k again? — Before October 2026","outcomes": [{ "name": "Yes", "price": 0.055, "implied_prob": 0.055, "devigged_prob": 0.0524 },{ "name": "No", "price": 0.945, "implied_prob": 0.945, "devigged_prob": 0.9476 }],"volume": 3678944.42,"liquidity": 1104224.42,"close_time": "2026-10-31T03:59:00Z","url": "https://kalshi.com/markets/kxbtcmax150/kxbtcmax150-25-26oct31-149999.99","scraped_at": "2026-07-05T14:57:29.522Z"}]
With include_orderbook: true, each item additionally gets:
"orderbook": {"bids": [{ "price": 0.98, "size": 357.0 }, "... up to 10 levels"],"asks": [{ "price": 0.99, "size": 45095.0 }, "... up to 10 levels"]}
Output schema
| Field | Type | Notes |
|---|---|---|
platform | string | "polymarket" or "kalshi" |
market_id | string | Polymarket condition ID (or slug if no condition ID) / Kalshi ticker |
question | string | Human-readable market question |
outcomes[].name | string | e.g. "Yes" / "No", or a named outcome for multi-way markets |
outcomes[].price | float (0–1) | Raw last/mid price, i.e. the platform's own implied probability including vig |
outcomes[].implied_prob | float (0–1) | Same as price — kept as its own explicit field per the output contract |
outcomes[].devigged_prob | float (0–1) or null | Vig-removed fair probability (multiplicative method); all outcomes of one market sum to 1.0. null only if de-vig math wasn't possible (e.g. a single-outcome market) |
volume | float or null | Platform-reported traded volume |
liquidity | float or null | Platform-reported liquidity/open interest |
close_time | string (ISO 8601) or null | Market close/expiration time |
url | string or null | Link to the market on the platform's site |
scraped_at | string (ISO 8601) | When this actor fetched the data |
orderbook | object, only if include_orderbook: true | {bids: [{price, size}], asks: [{price, size}]}, top 10 levels each |
Pricing (pay-per-event)
| Event | Price | When it's charged |
|---|---|---|
market-result | $0.003 | Once per market returned |
orderbook-snapshot | $0.02 | Once per market, only when include_orderbook: true |
Plus Apify's own apify-actor-start synthetic event (first 5 seconds of compute free, platform-managed — never charged from this actor's code).
Example: 100 markets, no order books: 100 × $0.003 = $0.30. 100 markets + order books on 10 of them: $0.30 + 10 × $0.02 = $0.50.
If a run's cost would exceed the Max total charge USD you set for it, the actor stops producing further results at exactly that point — it never crashes and never produces unbilled/"free" results past the limit.
Error messages
- No
queryand nomarket_ids: "Missing search criteria: provide either 'query' ... or 'market_ids' ..." — add one of the two fields. - Unsupported
platformsvalue: "'platforms' contains unsupported value(s) [...]. Supported values are: ['kalshi', 'polymarket']." — fix the typo/remove the entry. max_marketsout of range: "'max_markets' must be between 1 and 200, got N." — pick a value in range.- Wrong type for any field (e.g.
queryas a number,market_idsas a bare string instead of an array): the message states the expected type and shows a corrected example. - 0 results with otherwise valid input: not an error — the run succeeds, but the log has an explicit
WARNING: 0 results produced despite valid input (...)line and the run's status message says so, so this is never a silent/invisible "nothing happened."
Known limitations (documented, not hidden)
- Kalshi has no public full-text market search endpoint.
querysearch against Kalshi resolves by substring-matching the query against the ~11k-entry/seriescatalog's titles/tickers (fetched once per run, cached in-memory for that run), then lists open markets for matching series. This is adequate for the actor's per-run cost/latency budget; a future improvement could cache the catalog in the key-value store across runs. - Order books are attached per-market, not per-outcome. For binary Yes/No markets this is the whole picture (No is fully determined by Yes); for a multi-outcome market, only the first outcome's book is attached.
market_idsmixing platforms: the input is a single flat list rather than per-platform lists, so an ID that doesn't belong to a requested platform simply yields nothing for that platform — this is intentional (see Input table), not an error.
Data sources (public, no API key, no scraping)
- Polymarket Gamma API:
https://gamma-api.polymarket.com/public-search,/markets - Polymarket CLOB API:
https://clob.polymarket.com/book - Kalshi trade-api v2:
https://api.elections.kalshi.com/trade-api/v2/{series,events,markets}
All are the same public, unauthenticated endpoints the platforms' own web UIs call — no login, no key, no ToS-risk scraping.