Prediction Markets
Kalshi and Polymarket are first-class venues behind the same SDK surface. The universal primitives — quote, trades, price_history, stream, orders — accept Kalshi tickers (e.g. KXBTCZ-26DEC31-T99000) and Polymarket event slugs (e.g. btc-updown-5m-1776537600) interchangeably. Venue-specific endpoints (settlement, markets, redeem) share a single venue= parameter.
Symbol formats
Pass any of these as a symbol:
| Form | Venue | Maps to |
|---|---|---|
KXBTCZ-26DEC31-T99000 | Kalshi | The YES side of that Kalshi market (ticker format: <SERIES>-<DATE>-<STRIKE>) |
KXBTCZ-26DEC31-T99000:YES | Kalshi | YES side (explicit canonical form) |
KXBTCZ-26DEC31-T99000:NO | Kalshi | NO side |
btc-updown-5m-1776537600 | Polymarket | YES-token of the event (default) |
btc-updown-5m-1776537600:yes | Polymarket | YES token (explicit) |
btc-updown-5m-1776537600:no | Polymarket | NO token |
107433204661... (77 digits) | Polymarket | Raw CTF token_id — passes through |
https://polymarket.com/event/btc-updown-5m-1776537600 | Polymarket | URL is stripped to slug |
Kalshi tickers resolve directly — no slug translation. A bare Kalshi ticker is treated as :YES for backward compatibility; use :NO to quote or trade the NO leg explicitly. Polymarket slugs resolve via Gamma (cached server-side for 10 minutes).
Discover markets
/v1/marketsUniform market discovery across both venues. Response shape is identical; set venue=kalshi or venue=polymarket.
Query parameters
| Parameter | Default | Description |
|---|---|---|
venue | polymarket | kalshi or polymarket |
slug | — | Direct lookup: Polymarket event slug, or Kalshi ticker |
q | — | Free-text search across ticker + question + rules |
tag | — | Tag filter (Polymarket tag slug, Kalshi series ticker) |
active | true | Only currently-trading markets. See Status filters below for the (active, closed) combinations that surface settled / all-status rows |
closed | false | Include resolved markets |
limit | 20 | Max markets per page (1–1000; server caps at 1000) |
offset | 0 | Pagination offset. Avoid for deep walks — use cursor instead |
cursor | — | Opaque pagination cursor returned by the prior response's next_cursor. Required for exhaustive catalog walks (/v1/markets?venue=kalshi has 40k+ open Kalshi rows alone). See Pagination below |
order | — (no sort) | volume | volume_week | volume_total | liquidity | end_date | start_date | created | competitive | closed_time. Empty default = let the venue choose (Kalshi: cursor-native catalog order; Polymarket: gamma's volume24hr). Asking for volume on Kalshi triggers a 4× over-fetch + client-side rerank, which is fine for one-shot top-N queries but breaks cursor-based exhaustive walks. Pass it only when you actually want top-by-volume |
expand | — | Comma-separated expansion modes. Currently supported: outcomes — emit one binary Market row per outcome token instead of one parent row per multi-outcome event. See Binary outcome expansion below |
Response shape
Both venues return the same Market shape:
{
"markets": [
{
"venue": "kalshi",
"slug": "KXBTCZ-26DEC31-T99000",
"question": "Bitcoin above $99,000 at 4pm ET on December 31?",
"description": "The market resolves to YES if ...",
"outcomes": ["Yes", "No"],
"outcome_token_ids": ["KXBTCZ-26DEC31-T99000:YES", "KXBTCZ-26DEC31-T99000:NO"],
"yes_token_id": "KXBTCZ-26DEC31-T99000:YES",
"no_token_id": "KXBTCZ-26DEC31-T99000:NO",
"condition_id": "KXBTCZ",
"neg_risk": false,
"volume_24h": 12453.12,
"end_date": "2026-12-31T21:00:00Z",
"url": "https://kalshi.com/markets/KXBTCZ-26DEC31-T99000"
}
],
"count": 1,
"venue": "kalshi",
"has_more": true,
"next_cursor": "CgwIwcnKzwYQ8Lrk_gISIktYRVJFRElWSVNJRUdBTUUtMjZNQVkwM1ZPTEhFRS1IRUU"
}The yes_token_id / no_token_id are immediately usable in seq.quote() / seq.buy() regardless of venue.
Pagination
For anything beyond the first page, use cursor pagination — offset works but burns network on Kalshi (each offset=N call walks from the start and skips). Two rules:
- Pass
limit=1000(the server cap) so each round-trip pulls a full page. - Pass back the response's
next_cursoras the next request's?cursor=…. Stop whenhas_more=falseornext_cursoris missing.
# Page 1
curl -H "Authorization: Bearer $SEQ_KEY" \
"https://api.sequencemkts.com/v1/markets?venue=kalshi&active=true&limit=1000"
# Page 2 — feed back the cursor from page 1's `next_cursor`
curl -H "Authorization: Bearer $SEQ_KEY" \
"https://api.sequencemkts.com/v1/markets?venue=kalshi&active=true&limit=1000&cursor=$CURSOR"The Python SDK exposes seq.iter_markets(...) and the Rust SDK exposes .fetch_all(), both of which thread cursors automatically until the catalog is exhausted. A full Kalshi open-universe walk is ≈44 round-trips at limit=1000 (Python: ~14 s, Rust: ~4 s).
Status filters — what you actually get back
The defaults silently filter to currently-trading markets. The full matrix:
active | closed | Kalshi internal | Polymarket internal | What you see |
|---|---|---|---|---|
true | false (default) | status=open | active=true&closed=false | Currently-trading only |
true | true | no status= filter | active=true&closed=true | Open + closed + settled (everything Kalshi/PM expose) |
false | true | status=settled | active=false&closed=true | Resolved / paid-out only |
false | false | no status= filter | gamma defaults | All statuses on Kalshi; PM defers to gamma's defaults |
Kalshi caveat: Kalshi's raw status enum has four values — initialized, open, closed (trading halted, not yet paid), settled. CC collapses to two buckets — our "active" → open, our "closed" → settled. To target initialized or closed-but-not-yet-settled rows, pass active=false&closed=false and filter client-side on the raw status field.
Binary outcome expansion (?expand=outcomes)
Polymarket multi-outcome events (Fed rate brackets, presidential nominees, World Cup winners, etc.) group N child markets under a single parent event. By default /v1/markets returns one row per event, with the bracket slate flattened into outcomes[] / outcome_token_ids[]. That's fine for browsing; for trading and pure-equivalence matching you want one first-class binary contract per outcome token.
Pass ?expand=outcomes to flat-map every multi-outcome event into N rows — one per bracket — each with:
| Field | Meaning |
|---|---|
symbol | Display-canonical: polymarket:<token_id> (or <ticker>:YES for Kalshi). On WS / /v1/quotes/{sym} / /v1/orders inputs the bare CTF token id (pure digits ≥60 chars) is accepted too — the slug resolver normalizes both. |
outcomes | Always ["Yes", "No"] — the row IS a binary contract. |
yes_token_id / no_token_id | Both populated (negRisk YES/NO complement). |
condition_id | The bracket's own conditionId. |
parent_condition_id | The parent event id, so a matcher can correlate child binaries back to the multi-outcome parent. |
parent_slug | The parent event's slug, e.g. "democratic-presidential-nominee-2028". |
question | "<event title> — <outcome label>". |
outcome_name | Resolved human label ("Liz Cheney"). null when the upstream label is a Polymarket pre-allocated placeholder slot — see below. |
raw_outcome_name | The upstream groupItemTitle, always populated. Use for auditing what was filtered. |
is_placeholder_outcome | true if the row is a placeholder slot (Person CC, Driver B, Other); false for real candidates. |
outcome_index | 0-based position within the parent event's markets[] as Polymarket emitted it. Stable across paginated requests. |
Placeholder outcomes — the contract for pure-equivalence matchers
Polymarket pre-allocates Person A–Person ZZ slots (plus "Other" and a few category cousins) on forward-looking events. They re-title these to a real candidate name when someone becomes notable; until that happens the placeholder IS what they emit on groupItemTitle and question. A pure-equivalence matcher MUST be able to skip them — otherwise it'll match polymarket:<Person CC token> against a real Kalshi binary with no semantic basis.
Skip rule:
m.is_placeholder_outcome === true → skip
m.outcome_name === null → skip (equivalent — kept null iff placeholder)
Within each event, expanded rows are sorted real-names-first so a paged consumer (limit=N) exhausts every real candidate before hitting any placeholder. The outcome_index field still reflects Polymarket's natural upstream position so callers that want the original ordering can re-sort by it.
Always-binary events (Kalshi rows, single-market Polymarket events) carry a populated symbol regardless of the flag and leave the placeholder fields unset — the concept doesn't apply.
Examples
curl -H "Authorization: Bearer $KEY" \
"https://api.sequencemkts.com/v1/markets?venue=polymarket\
&slug=democratic-presidential-nominee-2028&expand=outcomes&limit=200"{
"markets": [
{
"venue": "polymarket",
"symbol": "polymarket:54533043819946592547517511176940999955633860128497669742211153063842200957669",
"question": "Democratic Presidential Nominee 2028 — Gavin Newsom",
"outcomes": ["Yes", "No"],
"yes_token_id": "54533043819946592547517511176940999955633860128497669742211153063842200957669",
"no_token_id": "…",
"condition_id": "0x…",
"parent_condition_id": "30829",
"parent_slug": "democratic-presidential-nominee-2028",
"outcome_name": "Gavin Newsom",
"raw_outcome_name": "Gavin Newsom",
"is_placeholder_outcome": false,
"outcome_index": 0
},
{
"venue": "polymarket",
"symbol": "polymarket:67834126955354466786049932246962827069514258320708123726234642047236874537300",
"question": "Democratic Presidential Nominee 2028 — Person P",
"outcomes": ["Yes", "No"],
"yes_token_id": "67834126955354466786049932246962827069514258320708123726234642047236874537300",
"no_token_id": "…",
"parent_slug": "democratic-presidential-nominee-2028",
"outcome_name": null,
"raw_outcome_name": "Person P",
"is_placeholder_outcome": true,
"outcome_index": 45
}
],
"count": 128,
"venue": "polymarket"
}The count reports binary rows, not events — a multi-outcome event with N brackets becomes N rows. The Democratic Nominee 2028 event has 128 markets: 44 real candidates (Newsom, AOC, Buttigieg, …) plus 84 placeholders.
Subscribing to live books for every real outcome
Combine ?expand=outcomes with the unified /v1/stream WS to keep an order book in sync for every real candidate on a multi-outcome event with one connection:
# 1. Discover real-name binary outcomes (skip placeholders).
markets = seq.markets(
venue="polymarket",
slug="democratic-presidential-nominee-2028",
expand="outcomes",
)["markets"]
real_tokens = [m["yes_token_id"] for m in markets if not m.get("is_placeholder_outcome")]
# 2. Subscribe to book + prices for all of them on one WS.
channels = [f"book:{t}" for t in real_tokens] \
+ [f"prices:{t}" for t in real_tokens]
async for ch, data in seq.stream(channels):
if ch.startswith("book:"):
... # data = {bids: [[px,sz]…], asks: [[px,sz]…], ts_ns}
elif ch.startswith("prices:"):
... # data = {bid, ask, mid, bid_sz, ask_sz, spread_bps, ts_ns}Hot tokens deliver their first event in 0–3 s after subscribe; cold tokens trigger a peer-link SubscribeRemote to the EU edge and land their first event in ~900 ms. Channels are change-gated, so quiet outcomes go silent until the book moves — pair the WS with one GET /v1/quotes/{token_id} per token at startup if you need a guaranteed initial snapshot.
Empty book sides are real venue state
book.bids and book.asks are independent — either may be empty on a thin or one-sided prediction market. An empty side reflects real venue state at that moment (no resting buyers/sellers at that side), not a missing endpoint, a depth cap, or a Sequence bug. Both /v1/quotes and the WS book: channel return the full ladder up to your requested ?depth. The same holds for Kalshi tickers (<base>:YES / <base>:NO) and Polymarket token IDs.
This is verified bit-for-bit against the venue's own endpoints — clob.polymarket.com/book?token_id=… and api.elections.kalshi.com/trade-api/v2/markets/{ticker}/orderbook. When Sequence reports 0 bids × 9 asks, the venue reports the same 0 bids × 9 asks at that instant; when Sequence returns 21 bids × 44 asks, the venue does too. If you're seeing an unexpected empty side, your walker is reading real market state — not stale or truncated data.
If you're trying to fire a fill against a one-sided book, gate on book.bids (or asks, depending on side) being non-empty before sizing — the venue itself will reject the order otherwise.
Redeem winnings
/v1/markets/{slug}/redeem?venue={venue}Uniform settlement claim. Venue-specific execution:
- Kalshi auto-settles on the venue side. Returns immediately with
mode="auto_settled"— no action taken, nothing to do. Any payout is already reflected in your USD balance. - Polymarket submits an authenticated request to the CLOB relayer using your stored L2 credentials. Returns
mode="relayer_submitted"; the on-chain USDC transfer completes asynchronously (typically minutes).
Response shape
{
"mode": "auto_settled",
"venue": "kalshi",
"slug": "KXBTCZ-26DEC31-T99000",
"condition_id": null,
"token_ids": null,
"note": "Kalshi auto-settles resolved markets. No action required; any payout is already reflected in your balance."
}{
"mode": "relayer_submitted",
"venue": "polymarket",
"slug": "fed-decision-in-october",
"condition_id": "0xc62e545a...",
"token_ids": ["107433204661...", "186571669590..."],
"note": "Polymarket relayer accepted redemption. USDC credit posts on-chain once the relayer sweeps — typically within a few minutes."
}mode values
| Mode | Meaning |
|---|---|
auto_settled | Venue settles automatically (Kalshi). No action was taken. |
relayer_submitted | Polymarket relayer accepted the request. On-chain settlement async. |
noop | Nothing to redeem for this identity on this market. |
Get settlement
/v1/settlement/{slug}Ground-truth resolution data. For Polymarket this includes an optional Chainlink underlying curve (crypto Up/Down markets). Kalshi settlements come from the venue's own determination channel.
Path parameter
| Parameter | Description |
|---|---|
slug | Kalshi ticker or Polymarket event slug / URL. Raw CTF token_ids are rejected for Polymarket (metadata lookup needs a slug). |
Query parameters
| Parameter | Default | Description |
|---|---|---|
underlying | true | Include the Chainlink underlying curve + anchor/final (Polymarket crypto markets only). false skips the on-chain RPC calls (faster). |
Response shape
{
"slug": "btc-updown-5m-1776537600",
"question": "Bitcoin Up or Down - April 18, 2:40PM-2:45PM ET",
"yes_token_id": "107433204661...",
"no_token_id": "186571669590...",
"condition_id": "0xc62e545a...",
"neg_risk": false,
"status": "resolved",
"resolved_outcome": "Down",
"outcome_source": "polymarket_gamma",
"yes_price": 0.0,
"no_price": 1.0,
"event_start_s": 1776537600,
"event_end_s": 1776537900,
"resolution_source": "https://data.chain.link/streams/btc-usd",
"underlying": {
"symbol": "BTC-USD",
"source": "chainlink_price_feed_polygon",
"anchor_price_usd": 75569.89,
"final_price_usd": 75543.11,
"agrees_with_settlement": true,
"points": [...]
}
}Status values
| Status | Meaning |
|---|---|
open | Market is currently trading |
resolving | Event window ended; settlement in flight (UMA liveness, Kalshi determination delay) |
resolved | resolved_outcome is set; payout has been finalized |
settled | Kalshi only: determination + settlement both complete |
Kalshi's PredictionSpec.resolved_side is populated from the determined lifecycle event (true = YES won, false = NO won, null = void/scalar). The venue edge propagates this to CC within seconds of the determined event, not waiting for the 15-min discovery refresh.
Discover new and resolved markets in real time
Two push-based WS streams surface lifecycle events to clients without polling:
GET /v1/ws/new_markets— every freshly-minted Polymarket / Kalshi market, with slug, outcomes, tick size, and fee schedule (Polymarket enriched via CLOB before broadcast).GET /v1/ws/market_resolved— every market whose outcome was determined and paid out.
Both replay the last 128 events on connect, then stream live. Deduped across multi-region edges. See Real-Time Streams → Prediction-Market Lifecycle for frame shape and auth details. SDK wrappers: seq.stream_new_markets() / seq.stream_resolved_markets().
Settlement is reflected in /v1/positions automatically
The same resolution pipeline that drives /v1/ws/market_resolved also updates GET /v1/positions:
- Each event you see on
/v1/ws/market_resolvedis persisted to amarket_resolutionstable and loaded into an in-memory cache on CC. - A settlement worker flips every matching
positions_v2row tolifecycle.state = "resolved"with the terminaleconomic_value_usd_1e9(qty × $1 for the winning side, 0 for the losing side). settled: trueandsettled_mark_usd_1e9land on the position the instant the telemetry arrives — the cache is read-through so the API flips before the DB UPDATE even commits.
This fixes the settlement-window UX glitch where Kalshi winners lingered at qty>0, value=$0 for hours waiting on venue cleanup, and where Polymarket CTF losers stayed at qty>0 forever (CTF tokens don't burn). Clients rendering /v1/positions no longer need to post-filter against a separate resolution feed — see Portfolio → Position fields.
Universal primitives on prediction markets
quote, price_history, stream, and orders accept Kalshi tickers or Polymarket slugs identically:
GET /v1/quotes/KXBTCZ-26DEC31-T99000 # Kalshi YES NBBO
GET /v1/quotes/KXBTCZ-26DEC31-T99000:NO # Kalshi NO NBBO
GET /v1/quotes/btc-updown-5m-1776537600 # Polymarket YES NBBO
GET /v1/quotes/btc-updown-5m-1776537600:no # Polymarket NO NBBO
GET /v1/price_history/KXBTCZ-26DEC31-T99000?range=1h # bars
POST /v1/orders # body.instrument.symbol = ticker or slug
Internally the CC resolves slugs → token_id once per request (cached), then hits the same NBBO / trade ring / SOR path as CEX symbols.
For /v1/orders, the instrument type is auto-classified — you don't need to swap instrument.type from spot to prediction when the symbol is a prediction market.
Venue capability matrix
The uniform amend / decrease / batch verbs work on both venues, but execute differently. Inspect result.mode to branch on queue-sensitivity:
| Verb | Kalshi | Polymarket |
|---|---|---|
submit (buy/sell) | ✅ Native | ✅ Native (CTF Exchange, EIP-712 signed) |
cancel | ✅ Native | ✅ Native |
amend | cancel_replace through SDK/API today; transport supports native atomic /amend below this layer | cancel_replace (no native amend endpoint) |
decrease | cancel_replace_shrink through SDK/API today; transport supports native /decrease below this layer | cancel_replace_shrink |
batch | Kalshi /portfolio/orders/batched supports 20-op atomic in the transport. SDK graph batching currently serializes. | Polymarket CLOB POST /orders supports up to 15 orders with per-order results in the transport. SDK graph batching currently serializes. |
redeem | ✅ auto_settled (no-op success) | ✅ relayer_submitted (real relayer POST) |
GTT (expiration_ts) | ✅ Native | ❌ Not supported |
STP (taker_at_cross) default | ✅ On | ❌ CTF Exchange controls STP server-side |
| Reduce-only | ✅ Native | ❌ Not applicable |
Kalshi-specific details
Tick schedule
Kalshi markets use a tiered tick schedule advertised via price_ranges. Rails (0.00-0.05, 0.95-1.00) typically trade on a 0.0001 grid; the middle band (0.05-0.95) is 0.01. The edge rounds orders to the band-appropriate tick; PairSpec.price_step_1e9 advertises the coarsest tick for CC grid validation.
Authentication
Kalshi uses RSA-PSS over SHA-256 with MGF1 signing. Three headers on every authenticated request:
KALSHI-ACCESS-KEY— your API key IDKALSHI-ACCESS-TIMESTAMP— current Unix millisecondsKALSHI-ACCESS-SIGNATURE— base64-encoded signature oftimestamp + METHOD + path(without query params)
The WebSocket handshake uses the same auth. Stored in venue_credentials with api_key = key ID, api_secret = PEM-encoded RSA private key.
Settlement
Kalshi auto-settles. When the market_lifecycle_v2::settled event fires, your USD balance already reflects any winning payout. seq.redeem(ticker, venue="kalshi") returns auto_settled — informational, no action.
Polymarket-specific details
CTF Exchange contracts
Polymarket runs two matching contracts on Polygon:
0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E— regular markets0xC5d563A36AE78145C45a50134d48A1215220f80a— neg-risk markets
The edge selects automatically based on each token's neg_risk flag from Gamma.
Authentication
Polymarket uses EIP-712 over secp256k1 for orders (signed by your EVM private key) plus HMAC-SHA256 for L2 API calls. L2 credentials are derived on first connect via /auth/derive-api-key. Stored in venue_credentials with api_key / api_secret / passphrase (L2) + extra_json.signer_private_key (L1 EVM key).
Redemption
After a Polymarket market resolves, winning CTF outcome tokens have to be redeemed on-chain for USDC. seq.redeem(slug, venue="polymarket") submits an authenticated POST to /relayer/submit; the relayer executes the on-chain call within minutes.
Maker rewards
Polymarket pays liquidity providers on select markets — a per-market USDC rate per day, split among makers whose resting orders sit inside a spread band. The CC mirrors Polymarket's /rewards/markets/multi into a local cache (5 min refresh) so MM strategies can filter on eligibility and reason about accrual.
/v1/polymarket/rewardsLists every token currently in an active rewards program.
/v1/polymarket/rewards/:token_idReturns the rewards config for one token, or 404 if not in any program.
{
"token_id": "48331043336612883...",
"condition_id": "0x...",
"rewards_min_size": 50.0,
"rewards_max_spread": 3.0,
"rate_per_day_usdc": 120.0,
"current_spread": 1.5,
"reward_end_unix_s": 1798329600
}Use this to:
- Filter MM strategies to only rewards-eligible markets (
rate_per_day_usdc > 0). - Estimate daily accrual:
rate_per_day_usdc × (your_resting_notional / total_resting_notional), assuming you're inside the spread band. - Honest P&L:
spread_capture + accrued_rewards— rewards are real USDC, and not tracking them under-reports MM performance.
SDK:
rewards = seq.polymarket_rewards() # all active
cfg = seq.polymarket_rewards("12345...") # one tokenlet all = seq.polymarket_rewards().await?;
let cfg = seq.polymarket_rewards_for("12345...").await?;Fill-level reward attribution (which of YOUR fills earned how much) settles server-side at Polymarket — the formulas depend on per-fill spread + midpoint proximity integrated over the reward window. The endpoint above exposes the config; exact accrual should be reconciled from Polymarket's /rewards/user endpoint on a daily cadence.
Underlying source (crypto Up/Down markets)
The settlement response's underlying field uses the on-chain Chainlink Price Feed on Polygon. Polymarket's crypto Up/Down contracts settle against Chainlink Data Streams — same aggregation, different publication product. The two converge sub-basis-point in normal conditions; the agrees_with_settlement flag surfaces the ~0.5–2% of cases where they disagree. For ML training: use resolved_outcome as the label, drop rows where agrees_with_settlement is false.