Skip to content

@mostlyrightmd/markets/market-data

A caller-supplied value violates a market-data contract, caught before I/O.

The market-data twin of Python’s pre-I/O ContractError usage: an unparseable market reference, an unfiltered listing sweep, an outcome the market does not offer. Carries the teaching fields (field / expected / actual / location) so the caller gets the fix, not just a refusal.

  • MarketDataError

new MarketContractError(message, options): MarketContractError

string = ""

ContractErrorOptions = {}

MarketContractError

MarketDataError.constructor

readonly code: string

Stable string enum for branching without parsing message text.

MarketDataError.code

static defaultErrorCode: string = "CONTRACT_ERROR"

MarketDataError.defaultErrorCode


An interval value is outside the venue’s supported resolution set.

The price-history verbs take interval: "1m" | "1h" | "1d". A value the venue does not serve is a fixable typo, so the error hands back the fix: the message names the whole valid set and supported carries it structurally, so a caller (or an agent) can branch without parsing prose.

try {
await candles(ticker, { interval: "5m", ... });
} catch (err) {
if (err instanceof UnsupportedResolutionError) {
console.log(err.venue, err.interval, err.supported);
}
}
  • MarketDataError

new UnsupportedResolutionError(message, options): UnsupportedResolutionError

string = ""

UnsupportedResolutionErrorOptions = {}

UnsupportedResolutionError

MarketDataError.constructor

readonly code: string

Stable string enum for branching without parsing message text.

MarketDataError.code

static defaultErrorCode: string = "UNSUPPORTED_RESOLUTION"

MarketDataError.defaultErrorCode

readonly interval: null | string

readonly supported: null | readonly string[]

Normalised to string[] so the payload is JSON-stable whatever iterable arrived.

readonly venue: null | string


A verb exists in the shared venue grammar but this venue has no such concept.

The market-data verbs speak one grammar across venues, but the venues’ own hierarchies differ: Kalshi is Series -> Event -> Market, Polymarket is Event -> Market with no series tier at all. So polymarket.series() exists (the grammar is shared) and raises this rather than returning an empty result, which would read as “no series right now” instead of “never any”. remedy names the verb to call instead.

  • MarketDataError

new VenueCapabilityError(message, options): VenueCapabilityError

string = ""

VenueCapabilityErrorOptions = {}

VenueCapabilityError

MarketDataError.constructor

readonly capability: null | string

readonly code: string

Stable string enum for branching without parsing message text.

MarketDataError.code

static defaultErrorCode: string = "VENUE_CAPABILITY"

MarketDataError.defaultErrorCode

readonly remedy: null | string

readonly venue: null | string

readonly capturedAt: Date

Client-clock snapshot instant, used when the venue stamps none.

readonly marketId: string


readonly endTs: number

Window end in UNIX seconds.

readonly fidelityMinutes: number

Bucket width in minutes — the unit the venue’s fidelity parameter takes.

readonly startTs: number

Window start in UNIX seconds.


readonly optional limit: number

Rows requested per page. Default 500.


readonly optional active: boolean

Tri-state active hint, with the same omit-vs-explicit-false contract.

readonly optional closed: boolean

Tri-state settled/unsettled hint. undefined omits the filter entirely (the endpoint’s own default applies) while false sends an explicit false — those are different requests.

readonly optional limit: number

Rows requested per page. Default 100.


readonly endTs: number

readonly periodIntervalMinutes: number

Bucket size in MINUTES (Kalshi documents 1, 60, 1440).

readonly startTs: number


readonly optional limit: number

readonly optional seriesTicker: string

readonly optional status: string

Upstream-validated status filter. Anything else returns HTTP 400.

readonly optional withNestedMarkets: boolean


readonly optional category: string

readonly optional eventTicker: string

readonly optional limit: number

readonly optional minCloseTs: number

Lower bound on the market close timestamp, UNIX seconds.

readonly optional seriesTicker: string

readonly optional status: string

Upstream-validated status filter. Anything else returns HTTP 400.

readonly optional tickers: readonly string[]

Explicit market tickers; sent comma-joined.


readonly optional category: string

Category narrowing (e.g. "Economics") — the only narrowing upstream honors.

readonly optional includeProductMetadata: boolean

Ask upstream to inline product metadata.


readonly optional limit: number

readonly optional maxTs: number

readonly optional minTs: number


readonly interval: MarketInterval

readonly marketId: string

The Kalshi ticker. Carried through as a string, always.

readonly tier: KalshiTier

Which tier produced raw. This selects the key spelling and becomes the row’s source provenance tag — the two can never disagree.


readonly fromTime: Date

Window start, inclusive.

readonly interval: MarketInterval

Bucket width: 1m | 1h | 1d.

readonly toTime: Date

Window end.


readonly optional all: boolean

Accept every event the venue lists. Required when no other filter is given.

readonly optional series: string

Series ticker to narrow to (e.g. "KXHIGHNY").

readonly optional status: string

One of open / closed / settled / unopened, validated pre-fetch.


readonly optional all: boolean

Accept the full unnarrowed universe. Required when no other filter is given.

readonly optional category: string

Venue category to narrow to.

readonly optional event: string

Event ticker to narrow to (e.g. "KXHIGHNY-26MAY29").

readonly optional historical: boolean

Read from the venue’s historical tier instead of the live one, for markets that closed before the live retention window. Requires series — that tier reaches back far enough that an unnarrowed walk of it is exactly the bulk retrieval the venue’s terms bar. Rows come back stamped source: "kalshi.historical".

readonly optional series: string

Series ticker to narrow to (e.g. "KXHIGHNY").

readonly optional status: string

One of open / closed / settled / unopened, validated pre-fetch.


readonly optional depth: number

Levels per side to request. Default 50; the venue accepts [1, 1000].


What a Kalshi reference string actually identified.

level is derived from the URL shape, never guessed: a series link carries no market ticker, so coercing one out of it would be the silent-wrong- identifier failure. ticker is populated only at level === "market".

readonly eventSlug: null | string

readonly level: "event" | "series" | "market"

readonly series: null | string

readonly ticker: null | string


readonly optional all: boolean

Accept the full unnarrowed catalog. Required when category is omitted.

readonly optional category: string

Venue category to narrow to (e.g. "Economics").


readonly marketId: string

readonly tier: KalshiTier


readonly fromTime: Date

Window start, inclusive.

readonly toTime: Date

Window end, exclusive — rows at exactly toTime fall outside the window.


One OHLC bucket — schema.markets.candles.v2, 23 columns.

Kalshi always returns the full column set (trade OHLC + bid OHLC + ask OHLC + volume + open interest) because bid/ask is often the only signal on an illiquid market. Polymarket serves a last price per bucket only, so its bid/ask, volume and open-interest columns are null — one shared column set keeps a cross-venue concatenation meaningful, and null means exactly “the venue served nothing”, never zero.

readonly ask_close: null | number

readonly ask_high: null | number

readonly ask_low: null | number

readonly ask_open: null | number

Kalshi yes_ask OHLC; null on Polymarket — probability in [0,1].

readonly bid_close: null | number

readonly bid_high: null | number

readonly bid_low: null | number

readonly bid_open: null | number

Kalshi yes_bid OHLC; null on Polymarket — probability in [0,1].

readonly bucket_start_utc: string

Bucket start as an ISO-8601 UTC instant. Kalshi serves end_period_ts (the period end) and is normalized to the start; the Polymarket CLOB t field is already a bucket start.

readonly close: null | number

readonly close_native: null | number

readonly high: null | number

readonly high_native: null | number

readonly interval: MarketInterval

readonly low: null | number

readonly low_native: null | number

readonly market_id: string

Kalshi ticker | Polymarket CLOB token id. Always a string.

readonly open: null | number

Trade OHLC — probability in [0,1].

readonly open_interest: null | number

Units: contracts. Open contracts at bucket close. Kalshi only.

readonly open_native: null | number

Venue-native price. Kalshi cents, with sub-cent price tiers preserved as fractional cents; on Polymarket the native unit is already probability, so this equals the probability column.

readonly source: MarketDataSource

readonly venue: MarketVenue

readonly volume_contracts: null | number

Units: contracts. Contracts traded in the bucket. Kalshi only: null on Polymarket — the venue serves no candle volume, and absent is not zero.


readonly optional baseUrl: string

Override the Kalshi API base.

Security: this override exists for tests and proxies, and its value is the caller’s own responsibility. No user-supplied string ever reaches it implicitly — a pasted kalshi.com link is parsed offline for its ticker and the request is then built from the manifest-resolved base (fallback KALSHI_API_BASE), so a lookalike host cannot steer a request anywhere. When set, this override wins over the manifest.

readonly optional fetchImpl: (input, init?) => Promise<Response>(input, init?) => Promise<Response>

Inject fetch for tests or for a proxy wrapper. Defaults to global fetch.

MDN Reference

URL | RequestInfo

RequestInit

Promise<Response>

MDN Reference

string | URL | Request

RequestInit

Promise<Response>

readonly optional maxPages: number

Safety cap on pages walked per cursor-paginated call. Default 10 000.

readonly optional signal: AbortSignal

Abort the whole call. Threaded into every fetch this module issues.

readonly optional sleepBetweenMs: number

Politeness sleep between requests in ms. Default 100. Pass 0 to skip.

readonly optional sleepImpl: (ms, signal?) => Promise<void>

Injectable sleep. Defaults to a real setTimeout sleep; tests inject a recorder so a capped Retry-After can be asserted without actually waiting two minutes.

number

AbortSignal

Promise<void>

readonly optional timeoutMs: number

Per-attempt request timeout in ms. Default 30 000.

Without one, a server that accepts the connection and then never answers pins the call forever — fetch has no timeout of its own. Each attempt gets its own AbortController, cleared in a finally, and the caller’s own signal is composed onto it: aborting the caller aborts the attempt, and the timeout aborts only the attempt it belongs to. (Composed by listener rather than AbortSignal.any, which is not available everywhere this bundles.)

readonly optional userAgent: string

Override the User-Agent. Defaults to core’s userAgent() — never a literal.


One event — schema.markets.events.v1, 10 columns.

readonly close_time: null | string

Event close instant as an ISO-8601 UTC string.

readonly event_id: null | string

Kalshi event_ticker | Polymarket event id. Always a string when present; null when the venue served no id on the record.

readonly event_slug: null | string

Polymarket URL slug; Kalshi repeats its event_ticker here.

readonly market_count: null | number

Number of markets under the event as served.

readonly open_time: null | string

Event open instant as an ISO-8601 UTC string.

readonly series_ticker: null | string

Kalshi only. Null on Polymarket — the venue has no series tier.

readonly source: MarketDataSource

readonly status: null | string

Normalized status: open | closed | settled | unopened.

readonly title: null | string

readonly venue: MarketVenue


One market’s metadata — schema.markets.listing.v1, 19 columns.

readonly best_ask: null | number

Top-of-book ask — probability in [0,1].

readonly best_bid: null | number

Top-of-book bid — probability in [0,1].

readonly close_time: null | string

Market close instant as an ISO-8601 UTC string.

readonly event_id: null | string

Parent event: Kalshi event_ticker | Polymarket event id.

readonly last_price: null | number

Last traded price — probability in [0,1].

readonly liquidity_usd: null | number

Units: USD. Resting book value. Polymarket only (Gamma liquidityNum).

readonly market_id: null | string

Kalshi ticker | Polymarket Gamma market id. Always a string when present; null when the venue served no id on the record. A listing is a browse, so one malformed market reads null rather than taking the page down with it.

readonly market_slug: null | string

Polymarket URL slug; null on Kalshi.

readonly open_interest: null | number

Units: contracts. Open contracts. Kalshi only.

readonly open_time: null | string

Market open instant as an ISO-8601 UTC string.

readonly outcome_token_ids: null | string

Comma-joined Polymarket CLOB token ids, kept as strings — 77-digit ids lose precision if read as numbers. Null on Kalshi.

readonly outcomes: null | string

Comma-joined outcome names (Kalshi is always yes,no).

readonly series_ticker: null | string

Kalshi only. Null on Polymarket — the venue has no series tier.

readonly source: MarketDataSource

readonly status: null | string

Normalized status: open | closed | settled | unopened.

readonly title: null | string

Kalshi title | Polymarket question.

readonly venue: MarketVenue

readonly volume_contracts: null | number

Units: contracts. Lifetime contracts traded. Kalshi only.

readonly volume_usd: null | number

Units: USD. Lifetime notional. Polymarket only (Gamma volumeNum) and venue-served, not computed here. Null on Kalshi.


One book level, long format — schema.markets.orderbook.v2, 9 columns.

Kalshi quotes its book as YES bids and NO bids; the normalizer maps the NO side onto the yes-token ask by ask_price = 1 - no_bid_price, so both venues speak one vocabulary and a Kalshi book compares directly to a Polymarket one.

readonly captured_at: string

Snapshot instant as an ISO-8601 UTC string — the client clock for Kalshi (the venue stamps no book time), the venue timestamp for Polymarket.

readonly level: number

Depth rank: 0 = best (top of book), increasing away from the touch.

readonly market_id: string

Kalshi ticker | Polymarket CLOB token id. Always a string.

readonly price: null | number

Probability in [0,1].

readonly price_native: null | number

Venue-native price — Kalshi cents; on Polymarket equal to price.

readonly side: MarketBookSide

readonly size_contracts: null | number

Units: contracts. Resting size at this level.

readonly source: MarketDataSource

readonly venue: MarketVenue


One recurring-contract family — schema.markets.series.v1, 7 columns.

Kalshi only. Polymarket has no series tier at all, which is why polymarket.series() raises VenueCapabilityError rather than returning an empty frame that would read as “this venue has no series right now”.

readonly category: null | string

Venue category — the value the category= filter matches.

readonly frequency: null | string

Recurrence cadence as served by the venue.

readonly series_ticker: null | string

The recurring-contract identifier (e.g. the NHIGH family stem). Null when the venue served no ticker on the record: a browse must not fail on one malformed row.

readonly settlement_sources: null | string

Comma-joined venue settlement source names.

readonly source: MarketDataSource

readonly title: null | string

readonly venue: MarketVenue

Always kalshi — Polymarket has no series tier.


One executed trade — schema.markets.trades.v2, 10 columns.

readonly market_id: null | string

Kalshi ticker | Polymarket CLOB token id. Always a string when present, and nullable here unlike on the candle row: Polymarket’s Data-API interleaves every outcome of a market and each row carries its own asset, so a row served without one keeps a null id rather than borrowing its neighbour’s — which would attribute a NO print to the YES token.

readonly price: null | number

Probability in [0,1]. On Kalshi this is the YES price.

readonly price_native: null | number

Venue-native price — Kalshi cents; on Polymarket equal to price.

readonly side: null | string

Normalized lowercase outcome taken: yes | no on Kalshi, the outcome label on Polymarket.

readonly size_contracts: null | number

Units: contracts. Shares/contracts traded.

readonly source: MarketDataSource

readonly trade_id: null | string

Kalshi trade_id | Polymarket transactionHash.

readonly traded_at: string

Trade instant as an ISO-8601 UTC string.

readonly venue: MarketVenue

readonly volume_usd: null | number

Units: USD. Polymarket only, and derived as size_contracts * price — the venue serves no such field. Null on Kalshi, which publishes no USD notional for a fill and where inventing one would be a fabricated number.


readonly interval: MarketInterval

readonly marketId: string

The CLOB token id. Carried through as a string, always.


readonly fromTime: Date

Window start, inclusive.

readonly interval: MarketInterval

Bucket width: 1m | 1h | 1d.

readonly side: string

The outcome to price, matched case-insensitively against the market’s own.

readonly toTime: Date

Window end, exclusive.


readonly optional clobBaseUrl: string

Override the CLOB base. Same contract as gammaBaseUrl.

readonly optional dataBaseUrl: string

Override the Data-API base. Same contract as gammaBaseUrl.

readonly optional fetchImpl: (input, init?) => Promise<Response>(input, init?) => Promise<Response>

Inject fetch for tests or for a proxy wrapper. Defaults to global fetch.

MDN Reference

URL | RequestInfo

RequestInit

Promise<Response>

MDN Reference

string | URL | Request

RequestInit

Promise<Response>

readonly optional gammaBaseUrl: string

Override the Gamma base.

Security: this override exists for tests and proxies, and its value is the caller’s own responsibility. No user-supplied string ever reaches it implicitly — a pasted polymarket.com link is parsed offline for its slug and the request is then built from the manifest-resolved base (fallback GAMMA_API_BASE), so a lookalike host cannot steer a request anywhere. When set, this override wins over the manifest.

readonly optional maxPages: number

Safety cap on pages walked per paginated call. Default 10 000.

readonly optional signal: AbortSignal

Abort the whole call. Threaded into every fetch this module issues.

readonly optional sleepBetweenMs: number

Politeness sleep between requests in ms. Default 200. Pass 0 to skip.

readonly optional sleepImpl: (ms, signal?) => Promise<void>

Injectable sleep. Defaults to a real setTimeout sleep; tests inject a recorder so a capped Retry-After can be asserted without actually waiting two minutes.

number

AbortSignal

Promise<void>

readonly optional timeoutMs: number

Per-attempt request timeout in ms. Default 30 000.

Without one, a server that accepts the connection and then never answers pins the call forever — fetch has no timeout of its own, and in a browser tab that is a spinner that never stops. Each attempt gets its own AbortController, cleared in a finally, and the caller’s own signal is composed onto it by listener (AbortSignal.any is not available everywhere this bundles).


readonly optional all: boolean

Accept every event the venue lists. Required when no other filter is given.

readonly optional event: string

An event slug or a pasted polymarket.com event link, parsed offline.

readonly optional status: string

One of open / closed / settled / unopened, validated pre-fetch.


readonly optional all: boolean

Accept the full unnarrowed universe. Required when no other filter is given.

readonly optional event: string

An event slug or a pasted polymarket.com event link, parsed offline.

readonly optional status: string

One of open / closed / settled / unopened, validated pre-fetch.


readonly side: string

The outcome whose book to snapshot, matched case-insensitively.


What a Polymarket reference string actually identified.

slug is the identifier the link points at (the market slug for a market link, the event slug for an event link); eventSlug is the owning event when the link carried one, else null — a /market/{slug} shortlink carries no event context.

readonly eventSlug: null | string

readonly level: "event" | "market"

readonly slug: string


readonly marketId: string


readonly fromTime: Date

Window start, inclusive.

readonly optional side: string

Keep only this outcome, matched case-insensitively. Omit for every side.

readonly toTime: Date

Window end, exclusive — a trade at exactly toTime falls outside.


readonly all: boolean

The caller’s explicit full-sweep opt-in.

readonly verb: string

The public verb name, for the message.


Both identifiers a price verb needs, returned together so neither is guessed.

readonly conditionId: string

The Gamma condition id — what the Data-API /trades addresses by.

readonly tokenId: string

The CLOB token id — what /prices-history and /book address by.


  • ContractErrorOptions

optional interval: null | string

The rejected interval value exactly as the caller passed it.

optional supported: null | Iterable<string, any, any>

The venue’s supported set. Normalised to string[] on the instance.

optional venue: null | string

The venue whose set was violated ("kalshi" / "polymarket").


  • ContractErrorOptions

optional capability: null | string

The grammar verb / concept the venue has no analog for ("series").

optional remedy: null | string

What to call instead ("use events()") — the teaching remedy.

optional venue: null | string

The venue lacking the capability.

KalshiTier: "kalshi" | "kalshi.historical"

The Kalshi source identities this module emits.


MarketBookSide: "bid" | "ask"

Book side of the YES token. Mirrors the schema.markets.orderbook.v2 enum.


MarketDataSchemaId: typeof MARKET_DATA_SCHEMA_IDS[keyof typeof MARKET_DATA_SCHEMA_IDS]

The id string of any of the six market-data schemas.


MarketDataSource: "kalshi" | "kalshi.historical" | "polymarket.gamma" | "polymarket.clob" | "polymarket.data"

Per-row source identity. The dotted suffix names the tier, following the cli/cli.archive and cwop.live/cwop.cache idiom already in the SDK: a consumer reading a concatenated frame can see which endpoint served each row without a second column.

  • kalshi — the live Kalshi trade API.
  • kalshi.historical — the Kalshi /historical tier (a different wire vocabulary for the same semantics; see normalize.ts).
  • polymarket.gamma — Gamma market/event metadata.
  • polymarket.clob — the CLOB price-history and book endpoints.
  • polymarket.data — the Data-API trade tape.

MarketInterval: "1m" | "1h" | "1d"

Supported candle bucket widths. Mirrors Python _normalize.INTERVALS.


MarketVenue: "kalshi" | "polymarket"

The venue a row belongs to — the venue column.


NormalizedCandleRow: Omit<MarketCandleRow, "bucket_start_utc"> & object

A candle row as it leaves the normalizer, before schema validation.

readonly bucket_start_utc: string | null


NormalizedOrderbookRow: MarketOrderbookRow

A book level as it leaves the normalizer. captured_at is always known.


NormalizedTradeRow: Omit<MarketTradeRow, "traded_at"> & object

A trade row as it leaves the normalizer, before schema validation.

readonly traded_at: string | null

const CLOB_API_BASE: "https://clob.polymarket.com" = "https://clob.polymarket.com"

Polymarket CLOB API base — a separate host from Gamma, hosting /prices-history and /book. The market query parameter on /prices-history is the CLOB token id (the ERC-1155 asset id), not the Gamma market or condition id. Kept as a module-level literal because it is both a public export (do not remove) and the fallback argument resolveSourceUrl uses when the resolved catalog has no usable entry for "markets.polymarket_clob". Resolution happens at call time inside the CLOB fetchers (opts.clobBaseUrl winning over the manifest), never here at module scope.


const DATA_API_BASE: "https://data-api.polymarket.com" = "https://data-api.polymarket.com"

Polymarket Data-API base — a third host, distinct from both Gamma and CLOB, serving the executed-trade tape at /trades. Its market query parameter takes the Gamma conditionId, not a CLOB token id — the exact reverse of the CLOB /prices-history parameter of the same name. Passing the wrong one returns an empty array rather than an error, so the two are worth keeping straight at the call site. Kept as a module-level literal because it is both a public export (do not remove) and the fallback argument resolveSourceUrl uses when the resolved catalog has no usable entry for "markets.polymarket_data". Resolution happens at call time inside fetchDataApiTrades (once, before its offset loop; opts.dataBaseUrl winning over the manifest), never here at module scope.


const GAMMA_API_BASE: "https://gamma-api.polymarket.com" = "https://gamma-api.polymarket.com"

Polymarket Gamma API base — read-only public REST, keyless. Hosts /markets/keyset, /events/keyset, /markets/slug/{slug}, and /events/slug/{slug}. Kept as a module-level literal because it is both a public export (do not remove) and the fallback argument resolveSourceUrl uses when the resolved catalog has no usable entry for "markets.polymarket_gamma". Resolution happens at call time inside the Gamma fetchers (once per outer call, opts.gammaBaseUrl winning over the manifest), never here at module scope.


const GAMMA_CURSOR_PARAM: "after_cursor" = "after_cursor"

The only request parameter that advances a Gamma keyset walk.

A keyset response carries its continuation token under next_cursor, so the two obvious spellings to echo back are cursor and next_cursor. Both were probed against the real endpoint: each returns HTTP 200 carrying the first page again, with the same row ids, forever. Only after_cursor moves the window. A loop built on either of the other two never terminates and never errors, which is why this is a named constant rather than an inline string, and why the walk also breaks when a page yields zero new ids.


const KALSHI_API_BASE: "https://api.elections.kalshi.com/trade-api/v2" = "https://api.elections.kalshi.com/trade-api/v2"

Kalshi’s public REST base. Kept as a module-level literal because it is both a public export (do not remove) and the fallback argument resolveSourceUrl uses when the resolved catalog has no usable entry for "markets.kalshi". Resolution happens at call time inside each public fetcher (via resolveKalshiBase, once per outer call, threaded through pagination), never here at module scope. The ./trades twin keeps its own copy, but both halves pass the same "markets.kalshi" key, so one catalog edit repoints both at once; only the fallback literal below is per-file.


const KALSHI_CANDLE_CHUNK_BUCKETS: 4500 = 4500

The bucket count chunkWindow actually targets. The ~10% margin below KALSHI_MAX_CANDLES_PER_REQUEST exists so a boundary rounding (the server counting one bucket more than the client does at a chunk edge) can never trip the server cap.


const KALSHI_HISTORICAL_PATH_PREFIX: "/historical" = "/historical"

Path prefix for Kalshi’s historical tier — a path, not a second host. Verified 30 July 2026: /historical/* is served by the same host as the live tier, so one base URL with path-only variation covers both.


const KALSHI_MARKET_URL_HOSTS: ReadonlySet<string>

Hosts whose market links this module will parse for Kalshi.


const KALSHI_MARKET_URL_RE: RegExp

A kalshi.com market link. Path is /markets/{series} with an OPTIONAL second {event-title-slug} segment and nothing after it — a third segment is rejected even though the site soft-renders one. The per-market identity rides in the ?ticker= query param, extracted separately below.


const KALSHI_MAX_CANDLES_PER_REQUEST: 5000 = 5000

Kalshi’s per-request candlestick cap. Exceeding it is a hard HTTP 400 — no partial result, no silent truncation.


const KALSHI_TICKER_RE: RegExp

A Kalshi ticker: uppercase alphanumeric segments joined by - or . (the . appears in between-market strikes such as ...-B88.5). The first segment is required; /, ?, #, whitespace, and any lowercase or non-ASCII character are outside the class and cannot match.


const KALSHI_TIER_HISTORICAL: "kalshi.historical" = "kalshi.historical"

Source identity for a row read from Kalshi’s /historical tier. The dotted-suffix idiom mirrors cli/cli.archive and cwop.live/cwop.cache: a consumer joining a concatenated frame sees which tier served each row without a second column.


const KALSHI_TIER_LIVE: "kalshi" = "kalshi"

Source identity for a row read from Kalshi’s live tier.


const KALSHI_VENUE: MarketVenue = "kalshi"

Venue tag for Kalshi rows (both tiers).


const MARKET_DATA_SCHEMA_IDS: object

The six market-data wire schema ids, keyed by TS verb label.

readonly candle: "schema.markets.candles.v2" = "schema.markets.candles.v2"

readonly event: "schema.markets.events.v1" = "schema.markets.events.v1"

readonly listing: "schema.markets.listing.v1" = "schema.markets.listing.v1"

readonly orderbook: "schema.markets.orderbook.v2" = "schema.markets.orderbook.v2"

readonly series: "schema.markets.series.v1" = "schema.markets.series.v1"

readonly trade: "schema.markets.trades.v2" = "schema.markets.trades.v2"


const MARKET_INTERVALS: Readonly<Record<MarketInterval, number>>

Supported candle intervals -> bucket seconds. Mirrors Python INTERVALS verbatim so a caller never has to learn two resolution vocabularies.


const MAX_CANDLE_CHUNKS: 10000 = 10_000

Safety cap on the number of chunks chunkWindow will emit. A pathological window (decades at 1m) otherwise turns one call into an unbounded request storm against a public keyless endpoint.


const MVE_FILTER_EXCLUDE: "exclude" = "exclude"

The only value this SDK ever sends for Kalshi’s mve_filter query parameter.

Probed 30 July 2026: mve_filter is not validated upstream — mve_filter=bogus returns HTTP 200 with unfiltered results, in contrast to status=, which is validated (HTTP 400 invalid status filter). A typo in a caller-supplied value would therefore silently open the >400K multivariate-event-combo universe instead of erroring. The mitigation is structural rather than defensive: this is a module constant, and fetchKalshiMarkets exposes no mveFilter parameter at all.


const POLYMARKET_MARKET_URL_HOSTS: ReadonlySet<string>

Hosts whose market links this module will parse for Polymarket.


const POLYMARKET_MARKET_URL_RE: RegExp

A polymarket.com market link. Two accepted path families: /event/{event-slug}[/{market-slug}] — the canonical shape /market/{market-slug} — the singular per-market shortlink /markets/ (plural), /predictions/, /search and the site root are not market routes and do not match.


const POLYMARKET_SLUG_RE: RegExp

A Polymarket slug: lowercase alphanumeric segments joined by -. Same exclusions as above, plus uppercase — polymarket.com hard-404s an uppercased slug, so it is not an alias.


const POLYMARKET_SOURCE_CLOB: MarketDataSource = "polymarket.clob"

Source identity for Polymarket CLOB rows (/prices-history, /book).


const POLYMARKET_SOURCE_DATA: MarketDataSource = "polymarket.data"

Source identity for Polymarket Data-API rows (/trades).


const POLYMARKET_SOURCE_GAMMA: MarketDataSource = "polymarket.gamma"

Source identity for Polymarket Gamma rows (market/event metadata).


const POLYMARKET_VENUE: MarketVenue = "polymarket"

Venue tag for Polymarket rows.

bucketStartFromPeriodEnd(endPeriodTs, intervalSeconds): null | Date

Convert a Kalshi end_period_ts into a UTC bucket start.

Kalshi stamps each candle with the period end; the canonical timestamp across both venues is the bucket start, so the interval is subtracted here once rather than at every call site. Returns null (never a fabricated epoch) when the payload’s timestamp is missing or unparseable.

unknown

number

null | Date


chunkWindow(fromTime, toTime, interval, maxBuckets): [number, number][]

Split [fromTime, toTime) into request windows under the venue cap.

Kalshi refuses any candlestick request whose window spans more than KALSHI_MAX_CANDLES_PER_REQUEST buckets with a hard HTTP 400 — no partial result, no silent truncation — so a month of 1m candles is not a single request. This function does the arithmetic once, in one place, and KALSHI_CANDLE_CHUNK_BUCKETS leaves a margin under the cap so a boundary rounding at a chunk edge can never trip it.

The returned chunks are contiguous and non-overlapping and tile the window exactly: the first start is fromTime, the last end is toTime, and every interior edge is shared. The caller is expected to request them sequentially (the politeness floor lives in the client, not here) and to dedup on bucket_start_utc after concatenating, because the venue may return the bucket sitting on a shared edge in both adjacent chunks.

Date

Date

MarketInterval

number = KALSHI_CANDLE_CHUNK_BUCKETS

[number, number][]

(startTs, endTs) pairs in UNIX seconds.

a bound is not a valid Date, fromTime >= toTime, maxBuckets < 1, or the window would need more than MAX_CANDLE_CHUNKS requests.

interval is outside MARKET_INTERVALS.


coerceGammaStringList(value): string[]

Coerce a Gamma polymorphic list field into a list of strings.

Gamma returns outcomes / clobTokenIds / outcomePrices sometimes as a native list and sometimes as a JSON-encoded string ('["Yes", "No"]'). Both forms are handled; anything else returns an empty list rather than throwing, because a malformed metadata field must not take down a listing sweep.

Every element is coerced to a string. Token ids are 77-digit decimals: a numeric coercion would silently lose precision and every join against the id would then fail.

unknown

string[]


dollarStringToProbability(value): null | number

Parse a venue dollar string (e.g. "0.5500") into a probability.

Both Kalshi tiers and Polymarket express a price per contract in dollars in [0,1], which is the implied probability — so no scaling is applied here. Null, empty, and non-numeric inputs return null.

unknown

null | number


fetchClobBook(tokenId, opts): Promise<Record<string, unknown>>

Fetch the current CLOB order book for tokenId from /book.

string

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>>

the book record carrying market (the condition id), asset_id, timestamp (milliseconds, as a string), and the bids / asks level arrays.


fetchClobPricesHistory(tokenId, args, opts): Promise<Record<string, unknown>[]>

Fetch the CLOB price history for one token from /prices-history.

tokenId is a CLOB token id (the ERC-1155 asset id) — a decimal integer roughly 77 digits long. It is carried as a string throughout and sent as a query parameter, never concatenated into the path.

The endpoint serves {t, p} points and nothing else: no volume, no bid/ask, no open interest. That absence is the reason the candle row’s volume_contracts is null rather than zero downstream.

string

FetchClobPricesHistoryArgs

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

the raw price points; [] when the venue serves an empty history.


fetchDataApiTrades(conditionId, args, opts): Promise<Record<string, unknown>[]>

Fetch the executed-trade tape for conditionId from the Data-API.

market= on this endpoint takes the Gamma conditionId, not a CLOB token id — the reverse of fetchClobPricesHistory. Passing a token id returns an empty array rather than an error, so the two are worth keeping straight.

Pages by offset. Whether the host honours that parameter is not something this client can assume, so the walk also stops when a page contributes zero rows it has not already seen: a host that ignores the offset re-serves the first page, and only the dedup guard ends that.

string

FetchDataApiTradesArgs = {}

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Browser-direct — the Data-API answers Access-Control-Allow-Origin: * plus Access-Control-Allow-Credentials: true.


fetchGammaEventBySlug(slug, opts): Promise<Record<string, unknown>>

Fetch one Gamma event by slug from /events/slug/{slug}.

The event-level companion to fetchGammaMarketBySlug; an event groups the markets a single question resolves across and carries them inline, so naming an event costs one request rather than a filtered walk.

string

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>>


fetchGammaEventsKeyset(args, opts): Promise<Record<string, unknown>[]>

List Gamma events through the cursor-paginated /events/keyset.

The same walk as fetchGammaMarketsKeyset against the event collection.

FetchGammaKeysetArgs = {}

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Browser-direct — Gamma answers Access-Control-Allow-Origin: *.


fetchGammaMarketBySlug(slug, opts): Promise<Record<string, unknown>>

Fetch one Gamma market by slug from /markets/slug/{slug}.

The lookup behind “paste the link and tell me about this market”. The returned record’s outcomes and clobTokenIds are JSON-string-encoded arrays handed back exactly as received. It also carries conditionId, which is what the Data-API trade tape addresses by.

string

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>>

Gamma answers 404 for an unknown slug — “no such market” and “no data” are different answers and this client will not conflate them.


fetchGammaMarketsKeyset(args, opts): Promise<Record<string, unknown>[]>

List Gamma markets through the cursor-paginated /markets/keyset.

The successor to the legacy offset-paginated /markets, which caps at offset 10,000. Rows are returned raw: outcomes and clobTokenIds arrive JSON-string-encoded and stay that way here — decoding them, and pairing an outcome with its token id, belongs to the normalizer, which guards the positional length match.

FetchGammaKeysetArgs = {}

PolymarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Browser-direct — Gamma answers Access-Control-Allow-Origin: *.


fetchKalshiCandlesticks(ticker, args, opts): Promise<Record<string, unknown>[]>

Fetch OHLCV candlesticks from the live tier.

string

FetchKalshiCandlesticksArgs

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>


fetchKalshiCandlesticksAnyTier(ticker, args, opts): Promise<[Record<string, unknown>[], MarketDataSource]>

Fetch candlesticks from whichever tier serves ticker.

string

FetchKalshiCandlesticksArgs

MarketDataClientOptions = {}

Promise<[Record<string, unknown>[], MarketDataSource]>

[rows, tier], where the rows keep their tier’s native field names — the caller must normalize per tier.

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiEvents(args, opts): Promise<Record<string, unknown>[]>

Fetch Kalshi events (GET /events), walking cursor pagination.

FetchKalshiEventsArgs = {}

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiHistoricalCandlesticks(ticker, args, opts): Promise<Record<string, unknown>[]>

Fetch candlesticks from the historical tier.

GET /historical/markets/{ticker}/candlesticks — note there is no /series/{s} segment here, unlike the live tier.

The rows use different field names from the live tier (price.open vs price.open_dollars, volume vs volume_fp) with the same semantics. They are returned raw; tier-explicit normalization is the normalizer’s job, and mixing the two shapes through one suffix-fallback chain produces silently 100x-wrong prices.

string

FetchKalshiCandlesticksArgs

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>


fetchKalshiHistoricalMarket(ticker, opts): Promise<Record<string, unknown>>

Fetch one market from the historical tier (GET /historical/markets/{ticker}), which serves the markets the live tier answers 404 for.

string

MarketDataClientOptions = {}

Promise<Record<string, unknown>>


fetchKalshiHistoricalMarkets(args, opts): Promise<Record<string, unknown>[]>

Fetch settled markets from the historical tier (GET /historical/markets).

seriesTicker is required, not optional: this tier reaches back past the live retention window, and an unnarrowed walk of it is exactly the bulk retrieval Kalshi’s Data Terms of Use bar.

number

string

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiHistoricalTrades(ticker, args, opts): Promise<Record<string, unknown>[]>

Fetch fills from the historical tier (GET /historical/trades).

Same row shape as the live tape. The confirmed parameter set is ticker / limit / cursor only — this tier accepts no min_ts / max_ts bounds, so the whole tape comes back and any time narrowing is the caller’s to apply. Unsupported bounds are not forwarded: upstream would ignore them silently and the caller would read a narrowing that never happened.

string

number

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>


fetchKalshiMarket(ticker, opts): Promise<Record<string, unknown>>

Fetch the market object from the live tier (GET /markets/{ticker}).

string

MarketDataClientOptions = {}

Promise<Record<string, unknown>>


fetchKalshiMarketAnyTier(ticker, opts): Promise<[Record<string, unknown>, MarketDataSource]>

Fetch the market object from whichever tier serves ticker.

string

MarketDataClientOptions = {}

Promise<[Record<string, unknown>, MarketDataSource]>

[market, tier].

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiMarkets(args, opts): Promise<Record<string, unknown>[]>

Fetch Kalshi markets (GET /markets), walking cursor pagination.

Always sends mve_filter=exclude from MVE_FILTER_EXCLUDE. There is deliberately no mveFilter field on FetchKalshiMarketsArgs — the endpoint does not validate that parameter, so a caller typo would silently open the >400K multivariate-event-combo universe rather than raising.

FetchKalshiMarketsArgs = {}

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiOrderbook(ticker, args, opts): Promise<Record<string, unknown>>

Fetch the current book snapshot for ticker (GET /markets/{t}/orderbook).

string

number

MarketDataClientOptions = {}

Promise<Record<string, unknown>>


fetchKalshiOrderbooks(tickers, opts): Promise<Record<string, Record<string, unknown>>>

Fetch orderbooks for many tickers (GET /markets/orderbooks).

The batch sweep takes a comma-joined tickers= list, at most ORDERBOOK_BATCH_SIZE per request. An empty list issues no request.

readonly string[]

MarketDataClientOptions = {}

Promise<Record<string, Record<string, unknown>>>

Node / server only — see docs/market-data-browser-support.md.


fetchKalshiSeries(args, opts): Promise<Record<string, unknown>[]>

Fetch the Kalshi series catalog (GET /series).

This endpoint returns everything, in one response. Measured 30 July 2026: 12,330 series, 16,277,583 bytes uncompressed / 2.29 MB gzipped, in 0.66 s. It ignores limit and returns no cursor, so there is nothing to paginate and no server-side way to ask for less — category is the only narrowing that actually shrinks the payload. No limit is sent, because sending one would imply a narrowing that does not happen.

FetchKalshiSeriesArgs = {}

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>

the series array; [] when upstream answers series: null.

Node / server only — see the file header and docs/market-data-browser-support.md.


fetchKalshiTrades(ticker, args, opts): Promise<Record<string, unknown>[]>

Fetch fills from the live tape (GET /markets/trades), cursor-paginated.

string

FetchKalshiTradesArgs = {}

MarketDataClientOptions = {}

Promise<Record<string, unknown>[]>


fetchKalshiTradesAnyTier(ticker, args, opts): Promise<[Record<string, unknown>[], MarketDataSource]>

Fetch fills from whichever tier serves ticker.

minTs / maxTs bound the live request only — the historical tape accepts no time bounds, so a fallback returns the market’s whole tape and the caller filters.

string

FetchKalshiTradesArgs = {}

MarketDataClientOptions = {}

Promise<[Record<string, unknown>[], MarketDataSource]>

[rows, tier].

Node / server only — see docs/market-data-browser-support.md.


kalshiCandles(urlOrId, args, opts): Promise<DataResult<MarketCandleRow>>

Fetch OHLC candles for one Kalshi market over a time window.

Kalshi refuses any candlestick request spanning more than 5,000 buckets with a hard HTTP 400 — no partial result, no silent truncation — so a long window is split into chunks under that cap and requested sequentially. Sequential, not parallel: the client’s politeness floor only holds if the requests are actually spaced, and a burst against a keyless public endpoint is exactly what a venue rate-limits. The venue may serve the bucket sitting on a shared chunk edge in both adjacent chunks, so rows are deduped on bucket_start_utc and sorted ascending before they are stamped.

The tier is decided once, from the first chunk, and reused for every later chunk. A frame never carries two provenances: if the live tier 404s, the whole call routes to /historical and every row — plus the result’s provenance.source — is stamped kalshi.historical. The two tiers spell the same price legs differently, so a frame mixing them would carry silently mis-scaled numbers with nothing in the data to reveal it.

The candle window is not half-open at the end. Every other windowed verb in this SDK is [fromTime, toTime), but Kalshi’s end_ts is inclusive, so a bucket whose period ends exactly at toTime comes back in the result. Read bucket_start_utc if you are joining candles to a half-open tape: the last bucket’s start is one interval before toTime.

The caller reaches Kalshi under their own relationship with the venue; this SDK sends no key and persists nothing.

string

KalshiCandlesArgs

MarketDataClientOptions = {}

Promise<DataResult<MarketCandleRow>>

either bound is not a valid Date.

fromTime >= toTime.

interval is outside 1m / 1h / 1d.

urlOrId is not a Kalshi market reference.

Node / server only — Kalshi 403s any non-kalshi.com Origin, including chrome-extension:// and null, and 403s the preflight. A browser or extension needs a proxy. See docs/market-data-browser-support.md.


kalshiEventRow(raw, source): MarketEventRow

Map one Kalshi event record onto the event column contract.

Record<string, unknown>

MarketDataSource

MarketEventRow


kalshiEvents(args, opts): Promise<DataResult<MarketEventRow>>

List the events under a Kalshi series.

An event groups the markets that settle on one occurrence — one calendar day of a daily-high series, for instance — so this is the middle tier between kalshiSeries and kalshiMarkets.

KalshiEventsArgs = {}

MarketDataClientOptions = {}

Promise<DataResult<MarketEventRow>>

a schema.markets.events.v1 result with open_time / close_time as ISO-8601 UTC instants. A query that matches nothing resolves to a zero-row result with the same provenance stamp.

no filter and no all: true, or an invalid status.

Node / server only — see docs/market-data-browser-support.md.


kalshiListingRow(raw, source): MarketListingRow

Map one Kalshi market record onto the listing column contract.

The Polymarket-only columns (market_slug, outcome_token_ids, volume_usd, liquidity_usd) are null here rather than filled with a plausible Kalshi substitute: the venue serves no such value, and absent is not zero.

Record<string, unknown>

MarketDataSource

MarketListingRow


kalshiMarket(urlOrId, opts): Promise<DataResult<MarketListingRow>>

Look one Kalshi market up by ticker or by a pasted kalshi.com link.

Paste the link from the post you are checking, or type the ticker — both identify the same contract and resolve to the same single row.

The pasted link is never fetched. It is parsed offline for host and shape, the ticker is extracted from it, and the venue request is built from the resolved API address, so a lookalike host cannot steer a request anywhere.

Both tiers are searched: the live tier answers first, and a market older than its retention window falls through to the historical tier and comes back stamped source: "kalshi.historical". A ticker neither tier knows raises rather than resolving to an empty result — “no such market” and “no data” are different answers and this verb will not conflate them.

string

a Kalshi market ticker ("KXHIGHNY-26MAY29-T85") or a kalshi.com market link carrying ?ticker=.

MarketDataClientOptions = {}

Promise<DataResult<MarketListingRow>>

urlOrId is not a valid ticker, or is a link that identifies a series or an event rather than a single market.

Node / server only — see docs/market-data-browser-support.md.


kalshiMarkets(args, opts): Promise<DataResult<MarketListingRow>>

List Kalshi markets, narrowed by series, event, status, or category.

Prices come back as probabilities in [0,1] so a frame from this venue and a frame from another can be concatenated and compared without a unit conversion at the call site. The Polymarket-only columns (market_slug, outcome_token_ids, volume_usd, liquidity_usd) are null: Kalshi serves no such value, and this frame would rather say nothing than say something plausible and wrong.

Multivariate event combinations are always excluded. They are a >400,000 row combinatorial universe that no research question asks for, and there is no argument to let them back in.

KalshiMarketsArgs = {}

MarketDataClientOptions = {}

Promise<DataResult<MarketListingRow>>

a schema.markets.listing.v1 result, one row per market.

no filter and no all: true, an invalid status, or historical: true without series.

Node / server only — see docs/market-data-browser-support.md.


kalshiOrderbook(urlOrId, args, opts): Promise<DataResult<MarketOrderbookRow>>

Fetch the current order book for one Kalshi market, in long format.

Kalshi quotes its book as YES bids and NO bids. A resting NO bid at price p is an offer to sell YES at 1 - p, so the NO side is mapped onto the yes-token ask by ask_price = 1 - no_bid_price. That identity is what makes a Kalshi book directly comparable to a Polymarket one; without it the two venues’ books look like they disagree when they do not.

level is 0-based from the best price on each side (highest bid, lowest ask), assigned here rather than trusted from the wire, so the ordering is identical however the venue happened to sort its arrays.

A book is a live concept and there is no /historical book endpoint, so these rows are always source: "kalshi". captured_at is the client clock — the venue stamps the snapshot with no time of its own.

string

KalshiOrderbookArgs = {}

MarketDataClientOptions = {}

Promise<DataResult<MarketOrderbookRow>>

urlOrId is not a Kalshi market reference, or depth is outside [1, 1000].

Node / server only — see docs/market-data-browser-support.md.


kalshiSeries(args, opts): Promise<DataResult<MarketSeriesRow>>

Browse Kalshi’s recurring-contract catalog.

A series is the concept a researcher thinks in — “the NYC daily high family” — and each series owns the events and markets beneath it. Start here, then narrow with kalshiEvents and kalshiMarkets.

This endpoint answers with everything, in one response. Measured July 2026: 12,330 series, 16,277,583 bytes uncompressed / 2.29 MB gzipped, in 0.66 s. It ignores limit and returns no cursor, so category is the only narrowing that actually shrinks the payload. Those numbers are what the venue served when they were taken and can move as the catalog grows.

The caller reaches Kalshi under their own relationship with the venue; this SDK sends no key and stores nothing.

KalshiSeriesArgs = {}

MarketDataClientOptions = {}

Promise<DataResult<MarketSeriesRow>>

a schema.markets.series.v1 result, one row per series. A category the venue knows nothing about resolves to a zero-row result with the same provenance stamp — never null, never a bare array.

neither category nor all: true was given.

Node / server only — Kalshi 403s any non-kalshi.com Origin. See docs/market-data-browser-support.md.


kalshiSeriesRow(raw): MarketSeriesRow

Map one Kalshi series record onto the series column contract.

Record<string, unknown>

MarketSeriesRow


kalshiTrades(urlOrId, args, opts): Promise<DataResult<MarketTradeRow>>

Fetch the executed-trade tape for one Kalshi market over a time window.

volume_usd is null on every row. Kalshi publishes no USD notional for a fill, and deriving one from a contract count would invent a number the venue never served.

The live tape is bounded server-side by min_ts / max_ts. The /historical tape accepts no time bounds (its confirmed parameter set is ticker / limit / cursor), so on the fallback path the whole tape comes back and the window is applied here — sending unsupported bounds would be ignored upstream and read as a narrowing that never happened. The filter runs on both paths so the verb’s contract does not change with the tier that answered.

string

KalshiTradesArgs

MarketDataClientOptions = {}

Promise<DataResult<MarketTradeRow>>

either bound is not a valid Date.

fromTime >= toTime.

urlOrId is not a Kalshi market reference.

Node / server only — see docs/market-data-browser-support.md.


makeMarketDataResult<Row>(rows, source, schemaId, retrievedAt, droppedUnplaceable): DataResult<Row>

Wrap market-data rows in the shared DataResult envelope.

source and retrievedAt map onto the provenance vocabulary; data_version and quality_control do not apply to a market-data frame and are explicitly null rather than omitted, so the provenance key set is the same fixed five on every verb in the SDK.

Rows are frozen: a caller that mutates the array it was handed would corrupt a frame whose provenance still claims it came straight off the venue.

Row

readonly Row[]

MarketDataSource

string

string

number = 0

how many rows were dropped because the venue served no usable instant for them. Mirrors the Python frame’s df.attrs["rows_dropped_unplaceable"], and like it appears only when > 0.

DataResult<Row>


normalizeKalshiCandle(raw, options): NormalizedCandleRow

Normalize one Kalshi candlestick into a canonical candle row.

Trade OHLC, bid OHLC, ask OHLC, volume_contracts and open_interest are all present on every row: on an illiquid market the bid/ask legs are often the only signal, so they are never conditional.

Record<string, unknown>

KalshiCandleOptions

NormalizedCandleRow

tier is not a known Kalshi tier tag.

raw does not speak tier’s vocabulary.


normalizeKalshiOrderbook(raw, options): MarketOrderbookRow[]

Normalize a Kalshi book snapshot into long-format level rows.

Kalshi quotes its book as YES bids and NO bids. A resting NO bid at price p is an offer to sell YES at 1 - p, so the NO side is mapped onto the yes-token ask by the identity ask_price = 1 - no_bid_price. That transformation is what makes a Kalshi book directly comparable to a Polymarket book, and it is stated in the schema notes for side.

level is 0-based from the best price on each side (highest bid, lowest ask), assigned here rather than trusted from the wire, so the ordering is identical no matter how the venue happened to sort the array.

Record<string, unknown>

BookOptions

MarketOrderbookRow[]


normalizeKalshiTrade(raw, options): NormalizedTradeRow

Normalize one Kalshi trade print into a canonical trade row.

price is the YES price: the whole frame speaks probability of YES, and the printed NO price is always 1 - yes, so carrying it would be a redundant second unit rather than new information.

volume_usd is null on Kalshi. The venue serves no USD notional for a fill, and deriving one from a contract count would invent a number the venue never published.

Record<string, unknown>

KalshiTradeOptions

NormalizedTradeRow

raw spells its price leg the other tier’s way. The live tier’s bare yes_price is legacy integer cents, so a cross-tier read would publish a price scaled by 100.


normalizePolymarketBook(raw, options): MarketOrderbookRow[]

Normalize a Polymarket CLOB book snapshot into long-format level rows.

Unlike Kalshi, Polymarket quotes the token’s own bids and asks directly, so no side transformation is needed. level is 0-based from the best price on each side (highest bid, lowest ask), assigned here rather than trusted from the wire.

The venue stamps its own snapshot timestamp (milliseconds, as a string); it wins over capturedAt, which is the client-clock fallback used when the venue omits it.

Record<string, unknown>

BookOptions

MarketOrderbookRow[]


normalizePolymarketPricePoint(raw, options): NormalizedCandleRow

Normalize one Polymarket price tick into a canonical candle row.

The served t is already the bucket start — the opposite of Kalshi, which serves the period end. Subtracting the interval here would shift every Polymarket bucket by one slot and put the same trade in adjacent buckets across venues.

The endpoint serves a last price per bucket, so open, high, low and close all carry it; there is no bid/ask OHLC and no volume, and those columns are null rather than fabricated. Polymarket’s native unit is already probability, so each *_native equals its probability column.

Record<string, unknown>

PolymarketCandleOptions

NormalizedCandleRow


normalizePolymarketTrade(raw, options): NormalizedTradeRow

Normalize one Polymarket Data-API trade into a canonical trade row.

volume_usd is derived here as size_contracts * price; the venue serves no USD notional on a trade row. Both inputs are carried on the row so the derivation is auditable rather than hidden, and it is null whenever either input is missing — a partially-known notional is not a notional.

side carries the outcome ("yes" / "no" / the multi-outcome label), matching the Kalshi column. The venue’s own side field is the taker direction (BUY / SELL), a different axis that the shared trade vocabulary does not carry.

Record<string, unknown>

PolymarketTradeOptions

NormalizedTradeRow


parseKalshiMarketRef(urlOrId): string

Return the validated Kalshi market ticker named by urlOrId.

The link is never dereferenced — see parseKalshiRef. The returned ticker is safe to interpolate into a venue API path: it has been matched against the fully anchored KALSHI_TICKER_RE allowlist, which admits no /, ?, #, whitespace, or non-ASCII character.

A series- or event-level link throws rather than returning the series slug — a series slug is not a market ticker, and returning one would hand the caller a wrong-but-plausible identifier. Use parseKalshiRef when a non-market link is expected.

string

string


parseKalshiRef(urlOrId): KalshiRef

Resolve a Kalshi reference string to a validated, level-tagged identity.

A pasted kalshi.com link is validated for host and path shape and its identifier is pulled out. The link itself is never dereferenced.

A bare identifier (no http prefix) is taken as a market ticker: the caller asserted “market ref”, and Kalshi’s series / event / market tickers share one alphabet, so the string alone cannot discriminate them. URLs can be discriminated by shape, and are — a series or event link comes back tagged as such with ticker: null rather than having a market ticker invented for it.

string

KalshiRef

the value is not a string, is empty, is non-ASCII, is over-length, or matches neither the ticker allowlist nor a canonical kalshi.com market-link shape.


parsePolymarketEventRef(urlOrSlug): string

Return the validated Polymarket event slug named by urlOrSlug.

The link is never dereferenced — see parsePolymarketRef. Accepts an event link, an event+market link (the event slug is taken from the first segment), or a bare slug (taken as an event slug — the caller asserted “event ref”).

A /market/{market-slug} shortlink throws: it carries no event segment, so there is no event slug to return and inventing one would be the silent-wrong-identifier failure.

string

string


parsePolymarketMarketRef(urlOrSlug): string

Return the validated Polymarket slug named by urlOrSlug.

The link is never dereferenced — see parsePolymarketRef. The returned slug has been matched against the fully anchored POLYMARKET_SLUG_RE allowlist and is safe to interpolate into a venue API path.

For /event/{event-slug}/{market-slug} this returns the market slug (the last segment), not the event slug. For an event-only link it returns the event slug — an event link genuinely identifies an event, and the level is not silently hidden: call parsePolymarketRef and read .level when the caller needs to know which of the two it got.

string

string


parsePolymarketRef(urlOrSlug): PolymarketRef

Resolve a Polymarket reference string to a validated, level-tagged identity.

A pasted polymarket.com link is validated for host and path shape and its slug is pulled out. The link itself is never dereferenced.

Level is read from the URL shape: /event/{e}/{m} and /market/{m} are market-level, /event/{e} is event-level. A bare slug is taken as market-level — the caller asserted “market ref”, and Polymarket event and market slugs share one alphabet and cannot be told apart offline.

string

PolymarketRef

the value is not a string, is empty, is non-ASCII, is over-length, or matches neither the slug allowlist nor a canonical polymarket.com market-link shape.


polymarketCandles(urlOrSlug, args, opts): Promise<DataResult<MarketCandleRow>>

Fetch OHLC candles for one Polymarket outcome over a time window.

Paste the link (or type the slug) and name the side; the CLOB token id is resolved internally — see resolveSideToken.

The venue serves no volume on this endpoint. /prices-history answers {t, p} points and nothing else, so volume_contracts is null on every row — never 0. A zero would assert “nothing traded in this bucket”, which the venue never said. open_interest and all eight bid_* / ask_* legs are null for the same reason: absent is not zero.

The candle is flat. The endpoint serves a last price per bucket, not a true OHLC, so open === high === low === close within each bucket. That is the venue’s shape, not a bug in this SDK.

*_native equals the probability column. Polymarket’s native unit is already probability, so there is no second magnitude to carry — unlike Kalshi, whose native unit is cents.

Bucket start, and the asymmetry with Kalshi. The CLOB t value is taken as the bucket start, where Kalshi serves the period end and is shifted back. That reading is an assumption, not a probed certainty; if it is ever falsified, every Polymarket bucket shifts by one slot relative to a Kalshi one.

You reach Polymarket under your own relationship with Polymarket. Nothing here is persisted.

string

PolymarketCandlesArgs

PolymarketDataClientOptions = {}

Promise<DataResult<MarketCandleRow>>

either bound is not a valid Date.

fromTime >= toTime.

interval is outside 1m / 1h / 1d.

urlOrSlug is not a Polymarket reference, or side is not one of the market’s outcomes.

Browser-direct — CLOB answers Access-Control-Allow-Origin: * plus Access-Control-Allow-Credentials: true. See docs/market-data-browser-support.md.


polymarketEventRow(raw): MarketEventRow

Map one Gamma event record onto the event column contract.

series_ticker is null, always. The column exists because the other venue has a series tier; filling it here with the event id or the slug would be inventing a tier this venue does not have.

Record<string, unknown>

MarketEventRow


polymarketEvents(args, opts): Promise<DataResult<MarketEventRow>>

Browse Polymarket events — the top tier of this venue’s hierarchy.

An event groups the markets one question resolves across, and on this venue it is where a browse starts: there is no series above it. The series_ticker column is null on every row for exactly that reason — see polymarketSeries, which says so out loud.

status is not a field Polymarket serves. It is derived from the venue’s own closed / active / acceptingOrders booleans into the same four words the other venue uses, so one filter reads across both.

You reach Polymarket under your own relationship with Polymarket. Nothing here is persisted.

PolymarketEventsArgs = {}

PolymarketDataClientOptions = {}

Promise<DataResult<MarketEventRow>>

a schema.markets.events.v1 result, one row per event, with open_time / close_time as ISO-8601 UTC instants. A query that matches nothing resolves to a zero-row result with the same provenance stamp — never null, never a bare array.

no filter and no all: true, an invalid status, or an event that is not a recognisable slug or link.

Browser-direct — Gamma answers Access-Control-Allow-Origin: *, so this runs from a page with no proxy. See docs/market-data-browser-support.md.


polymarketListingRow(raw): MarketListingRow

Map one Gamma market record onto the listing column contract.

outcomes and clobTokenIds are two parallel arrays that Gamma serves JSON-string-encoded and that are positionally aligned. When their lengths disagree the record cannot be aligned at all, and this builder emits outcomes with outcome_token_ids null rather than indexing into it. A listing is a browse: one malformed market should not poison a whole page, and a null says truthfully that the pairing is unknown. resolveSideToken raises on the same input, because there an unnoticed misalignment would put a price on the wrong outcome.

The Kalshi-only columns (series_ticker, volume_contracts, open_interest) are null rather than filled with a plausible Polymarket substitute: the venue serves no such value, and absent is not zero.

Record<string, unknown>

MarketListingRow


polymarketMarket(urlOrSlug, opts): Promise<DataResult<MarketListingRow>>

Look a Polymarket market up by slug or by a pasted polymarket.com link.

Paste the link from the post you are reading, or type the slug — both identify the same contract and resolve to the same row.

The pasted link is never fetched. It is parsed offline for host and path shape, the slug is pulled out of it, and the venue request is built from the module’s own base address, so a lookalike host cannot steer a request anywhere.

An event-level link resolves to more than one row. Polymarket links come in two shapes, /event/{event}/{market} and the bare /event/{event}, and the second one is what people usually have to hand: it is the link the site shows and the link that gets shared. Refusing it would send the caller back to the site to find a narrower one, which defeats the point of accepting a link at all. So an event link resolves the event and returns every market under it, and this verb’s singular name is a promise about what you ask for, not about how many rows come back. Read rows.length if that matters, or pass the market-level link to guarantee one row.

An unknown slug surfaces the venue’s 404 rather than resolving to an empty result. “No such market” and “no data” are different answers and this verb will not conflate them.

string

PolymarketDataClientOptions = {}

Promise<DataResult<MarketListingRow>>

urlOrSlug is not a recognised slug or a canonical polymarket.com link. Thrown before any request exists.

Browser-direct — see docs/market-data-browser-support.md.


polymarketMarkets(args, opts): Promise<DataResult<MarketListingRow>>

List Polymarket markets, narrowed by event or status.

Each row carries outcomes and outcome_token_ids as two comma-joined, positionally aligned lists, which is what a caller needs to pick a side before asking for a book or a price history. Token ids stay strings: a CLOB token id is roughly 77 digits, and reading one as a number drops its low-order digits without raising anything.

When those two arrays disagree in length the record cannot be aligned at all, and this verb emits outcomes with outcome_token_ids null. The verbs that resolve a side raise on the same input instead, because there an unnoticed misalignment would put a price on the wrong outcome.

Prices are probabilities in [0,1] so a frame from this venue and a frame from the other can be concatenated and compared with no unit conversion at the call site. volume_usd here is venue-served — Gamma’s own volumeNum, not a figure computed here — unlike the trade-tape column of the same name, which is derived as size times price. The Kalshi-only columns (series_ticker, volume_contracts, open_interest) are null: this venue serves no such value, and absent is not zero.

You reach Polymarket under your own relationship with Polymarket. Nothing here is persisted.

PolymarketMarketsArgs = {}

PolymarketDataClientOptions = {}

Promise<DataResult<MarketListingRow>>

a schema.markets.listing.v1 result, one row per market.

no filter and no all: true, an invalid status, or an event that is not a recognisable slug or link.

Browser-direct — see docs/market-data-browser-support.md.


polymarketOrderbook(urlOrSlug, args, opts): Promise<DataResult<MarketOrderbookRow>>

Fetch the current order book for one Polymarket outcome, in long format.

Polymarket quotes the token’s own bids and asks directly, so no side transformation is needed — unlike Kalshi, whose NO bids are mapped onto the yes-token ask. Long format is what makes the two comparable: one row per level per side concatenates across venues and keeps the units honest.

level is 0-based from the best price on each side (highest bid, lowest ask), assigned here rather than trusted from the wire, so the ordering is identical however the venue happened to sort its arrays.

captured_at comes from the venue’s own millisecond timestamp field, so every row of one snapshot carries the same instant. When the venue omits it the client clock stands in.

string

PolymarketOrderbookArgs

PolymarketDataClientOptions = {}

Promise<DataResult<MarketOrderbookRow>>

urlOrSlug is not a Polymarket reference, or side is not one of the market’s outcomes.

Browser-direct — see docs/market-data-browser-support.md.


polymarketSeries(…_args): never

Always throws: Polymarket has no series tier.

Kalshi groups its recurring contracts into a series above the event, so kalshiSeries() is where a browse of that venue starts. Polymarket has no such tier at all — its hierarchy is Event -> Market, and an event is already the top. Start with polymarketEvents instead.

This verb exists rather than being omitted so the difference is discoverable. A missing export teaches nothing; a shared grammar with a silent hole in it teaches something false.

Every argument is accepted and ignored, so a caller copying a Kalshi call across gets the lesson rather than a complaint about an argument.

…readonly unknown[]

never

always. Carries venue, capability and remedy, and subclasses ContractError, so it is thrown before any I/O and existing contract handlers still catch it.


polymarketTrades(urlOrSlug, args, opts): Promise<DataResult<MarketTradeRow>>

Fetch the executed-trade tape for one Polymarket market over a time window.

The Data-API market= parameter takes the Gamma conditionId, not a CLOB token id — the exact reverse of the CLOB price-history parameter of the same name. Passing a token id returns an empty array rather than an error, which is a real footgun; this verb resolves the condition id itself so a caller never meets it.

volume_usd is derived, as size_contracts * price. The venue serves no USD notional on a trade row: its size is in shares/contracts, and Polymarket’s own “volume” fields mean three different things depending on which endpoint you asked. size_contracts is carried alongside so the derivation is auditable rather than hidden, and volume_usd is null whenever either input is missing — a partially-known notional is not a notional.

side carries the outcome (yes / no / the multi-outcome label), matching the Kalshi column. The venue’s own side field is the taker direction (BUY / SELL), a different axis the shared trade vocabulary does not carry.

The window is half-open, [fromTime, toTime), matching every other windowed verb in the SDK, and it is applied client-side because the endpoint takes no time bounds.

Naming a side this market does not offer throws, and it throws before the tape is requested. “No trades” and “no such outcome” are different answers: a typo’d side that filtered client-side would resolve to an empty tape indistinguishable from a quiet window. Naming a side costs the same single Gamma request as not naming one — see resolveSideToken.

string

PolymarketTradesArgs

PolymarketDataClientOptions = {}

Promise<DataResult<MarketTradeRow>>

either bound is not a valid Date.

fromTime >= toTime.

urlOrSlug is not a Polymarket reference, or side is not one of the market’s outcomes (the message names the ones that are).

Browser-direct — see docs/market-data-browser-support.md.


probabilityToCents(probability): null | number

Convert a probability in [0,1] to the Kalshi-native cents magnitude.

Kept as a float so sub-cent price tiers survive as fractional cents (0.567 -> 56.7) instead of being truncated.

null | number

null | number


requireNarrowing(activeFilters, options): void

Refuse an unnarrowed browse, before a socket is opened.

Kalshi’s open universe is 75,603 markets once multivariate event combos are excluded, and /series answers with all 12,330 series in a single 16.3 MB response (it ignores limit and returns no cursor, so there is no server-side way to ask for less). An unnarrowed call is therefore never a small mistake, and it is not only an ergonomics problem: Kalshi’s data terms bar systematic retrieval, so sweeping the venue by accident is a licensing exposure the caller did not choose.

all: true is the deliberate opt-in. It is a word the caller has to type, which is the point.

An empty or whitespace-only string counts as absent, not as a filter. The venue treats series: "" as no narrowing at all, so accepting it here would let the empty string walk straight past this gate and sweep the universe the gate exists to protect — the one input most likely to arrive from an unfilled form field.

Record<string, unknown>

every narrowing argument the verb accepts, mapped to the value it was called with. The keys define the valid set named in the error, so a new filter can never be added without appearing in the teaching message.

RequireNarrowingOptions

void

no filter was supplied and all is false.


requirePolymarketNarrowing(activeFilters, options): void

Refuse an unnarrowed browse, before a socket is opened.

How big the Polymarket universe actually is has not been measured by this SDK, and the figures in general circulation disagree with each other by orders of magnitude, so no row count appears in the message below. The absence is deliberate: a number the SDK cannot stand behind would read as authority it has not earned. What is true regardless of the count is that an unnarrowed keyset walk pages until the venue stops answering, which is never a small mistake, and that sweeping a venue by accident is a licensing exposure the caller did not choose.

all: true is the deliberate opt-in. It is a word the caller has to type, which is the point.

An empty or whitespace-only string counts as absent, not as a filter: a blank event narrows nothing at the venue, so accepting it would let the empty string walk past the gate and start the sweep it exists to prevent. The rule matches the Kalshi gate’s; only the message is venue-specific.

Record<string, unknown>

every narrowing argument the verb accepts, mapped to the value it was called with. The keys define the valid set named in the error, so a new filter can never be added without appearing in the teaching message.

boolean

string

void

no filter was supplied and all is false.


resolveOutcomeToken(outcomes, tokenIds, side): string

Resolve an outcome label to its positionally aligned CLOB token id.

Gamma serves outcomes and clobTokenIds as two parallel arrays. Indexing blindly is unsafe: Gamma also carries multi-outcome (non-binary) markets, so a length mismatch means the record cannot be aligned at all and a positional read would hand back some other outcome’s token.

unknown

unknown

string

string

the two arrays have different lengths.

side is not one of the market’s outcomes; the message names the outcomes that are on offer.


resolveSideToken(slugOrUrl, side, opts): Promise<ResolvedSide>

Resolve a slug (or a pasted market link) plus an outcome side to its CLOB token id, carrying the condition id alongside.

This is the entry contract the price verbs are built on: a caller who has a link and knows which side they mean never has to learn what a 77-digit ERC-1155 asset id is. One Gamma /markets/slug/{slug} request answers both.

Both identifiers come back, deliberately. /prices-history and /book take the token id; the Data-API /trades takes the condition id. Returning one and letting the caller derive the other is how a request ends up addressing the wrong thing and getting an empty array instead of an error.

Side matching is case-insensitive. A length mismatch between outcomes and clobTokenIds raises here, where polymarketListingRow emits an honest null for the same malformed market: a listing is a browse and one bad row must not poison a page, but a side-resolving read that guessed would put a price on the wrong outcome and say nothing.

string

string

PolymarketDataClientOptions = {}

Promise<ResolvedSide>

slugOrUrl is not a recognisable reference, or side is not one of the market’s outcomes (the message names the ones that are).

outcomes and clobTokenIds disagree in length, so the record cannot be aligned at all.


validateEnum(value, allowed, field): void

Reject a value outside allowed, suggesting the nearest valid one.

Kalshi validates status server-side and answers HTTP 400 on anything else, so checking here turns a transport error into a readable one — and it happens pre-fetch, so a typo costs no request.

undefined | string

readonly string[]

string

void

value is neither undefined nor in allowed.


validateInterval(interval, venue): number

Return the bucket length in seconds for interval.

An invalid resolution is a fixable typo, so the error hands back the fix: the message names the whole valid set rather than only saying no, and supported carries it structurally so an agent can branch without parsing prose.

string

string

number

interval is outside MARKET_INTERVALS.


validatePolymarketEnum(value, allowed, field): void

Reject a value outside allowed, suggesting the nearest valid one.

Polymarket serves no status field to validate against, so unlike the Kalshi twin this check is not standing in for a venue HTTP 400 — the vocabulary is entirely ours. That makes checking it here the only place a typo can be caught: an unrecognised status would otherwise silently match no row and return an empty result that looks like a real answer.

undefined | string

readonly string[]

string

void

value is neither undefined nor in allowed.