Sequence/docs

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:

FormVenueMaps to
KXBTCZ-26DEC31-T99000KalshiThe YES side of that Kalshi market (ticker format: <SERIES>-<DATE>-<STRIKE>)
KXBTCZ-26DEC31-T99000:YESKalshiYES side (explicit canonical form)
KXBTCZ-26DEC31-T99000:NOKalshiNO side
btc-updown-5m-1776537600PolymarketYES-token of the event (default)
btc-updown-5m-1776537600:yesPolymarketYES token (explicit)
btc-updown-5m-1776537600:noPolymarketNO token
107433204661... (77 digits)PolymarketRaw CTF token_id — passes through
https://polymarket.com/event/btc-updown-5m-1776537600PolymarketURL 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

GET/v1/markets

Uniform market discovery across both venues. Response shape is identical; set venue=kalshi or venue=polymarket.

Query parameters

ParameterDefaultDescription
venuepolymarketkalshi or polymarket
slugDirect lookup: Polymarket event slug, or Kalshi ticker
qFree-text search across ticker + question + rules
tagTag filter (Polymarket tag slug, Kalshi series ticker)
activetrueOnly currently-trading markets. See Status filters below for the (active, closed) combinations that surface settled / all-status rows
closedfalseInclude resolved markets
limit20Max markets per page (1–1000; server caps at 1000)
offset0Pagination offset. Avoid for deep walks — use cursor instead
cursorOpaque 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
expandComma-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:

json
{
  "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 paginationoffset works but burns network on Kalshi (each offset=N call walks from the start and skips). Two rules:

  1. Pass limit=1000 (the server cap) so each round-trip pulls a full page.
  2. Pass back the response's next_cursor as the next request's ?cursor=…. Stop when has_more=false or next_cursor is missing.
bash
# 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:

activeclosedKalshi internalPolymarket internalWhat you see
truefalse (default)status=openactive=true&closed=falseCurrently-trading only
truetrueno status= filteractive=true&closed=trueOpen + closed + settled (everything Kalshi/PM expose)
falsetruestatus=settledactive=false&closed=trueResolved / paid-out only
falsefalseno status= filtergamma defaultsAll 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:

FieldMeaning
symbolDisplay-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.
outcomesAlways ["Yes", "No"] — the row IS a binary contract.
yes_token_id / no_token_idBoth populated (negRisk YES/NO complement).
condition_idThe bracket's own conditionId.
parent_condition_idThe parent event id, so a matcher can correlate child binaries back to the multi-outcome parent.
parent_slugThe parent event's slug, e.g. "democratic-presidential-nominee-2028".
question"<event title> — <outcome label>".
outcome_nameResolved human label ("Liz Cheney"). null when the upstream label is a Polymarket pre-allocated placeholder slot — see below.
raw_outcome_nameThe upstream groupItemTitle, always populated. Use for auditing what was filtered.
is_placeholder_outcometrue if the row is a placeholder slot (Person CC, Driver B, Other); false for real candidates.
outcome_index0-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 APerson 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:

code
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

bash
curl -H "Authorization: Bearer $KEY" \
  "https://api.sequencemkts.com/v1/markets?venue=polymarket\
&slug=democratic-presidential-nominee-2028&expand=outcomes&limit=200"
json
{
  "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:

python
# 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

POST/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

json
{
  "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."
}
json
{
  "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

ModeMeaning
auto_settledVenue settles automatically (Kalshi). No action was taken.
relayer_submittedPolymarket relayer accepted the request. On-chain settlement async.
noopNothing to redeem for this identity on this market.

Get settlement

GET/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

ParameterDescription
slugKalshi ticker or Polymarket event slug / URL. Raw CTF token_ids are rejected for Polymarket (metadata lookup needs a slug).

Query parameters

ParameterDefaultDescription
underlyingtrueInclude the Chainlink underlying curve + anchor/final (Polymarket crypto markets only). false skips the on-chain RPC calls (faster).

Response shape

json
{
  "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

StatusMeaning
openMarket is currently trading
resolvingEvent window ended; settlement in flight (UMA liveness, Kalshi determination delay)
resolvedresolved_outcome is set; payout has been finalized
settledKalshi 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_resolved is persisted to a market_resolutions table and loaded into an in-memory cache on CC.
  • A settlement worker flips every matching positions_v2 row to lifecycle.state = "resolved" with the terminal economic_value_usd_1e9 (qty × $1 for the winning side, 0 for the losing side).
  • settled: true and settled_mark_usd_1e9 land 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:

code
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:

VerbKalshiPolymarket
submit (buy/sell)✅ Native✅ Native (CTF Exchange, EIP-712 signed)
cancel✅ Native✅ Native
amendcancel_replace through SDK/API today; transport supports native atomic /amend below this layercancel_replace (no native amend endpoint)
decreasecancel_replace_shrink through SDK/API today; transport supports native /decrease below this layercancel_replace_shrink
batchKalshi /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.
redeemauto_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 ID
  • KALSHI-ACCESS-TIMESTAMP — current Unix milliseconds
  • KALSHI-ACCESS-SIGNATURE — base64-encoded signature of timestamp + 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 markets
  • 0xC5d563A36AE78145C45a50134d48A1215220f80a — 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.

GET/v1/polymarket/rewards

Lists every token currently in an active rewards program.

GET/v1/polymarket/rewards/:token_id

Returns the rewards config for one token, or 404 if not in any program.

json
{
  "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:

python
rewards = seq.polymarket_rewards()                  # all active
cfg     = seq.polymarket_rewards("12345...")       # one token
rust
let all = seq.polymarket_rewards().await?;
let cfg = seq.polymarket_rewards_for("12345...").await?;
Note

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.