# Authentication (/authentication) PredictorSDK uses scoped API keys for authentication. Each key is scoped with permissions that control which endpoints you can access. ## Getting an API key [#getting-an-api-key] 1. Sign up at [app.predictorsdk.com](https://app.predictorsdk.com) 2. Navigate to **API Keys** in your dashboard 3. Click **Create API Key** and give it a name 4. Copy the key immediately — it won't be shown again ## Using your API key [#using-your-api-key] Include the key in the `Authorization` header as a Bearer token: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports" ``` Or use the TypeScript SDK: ```ts import { PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); ``` See [SDK Installation](/sdk/installation) for all supported languages. ## Permissions [#permissions] API keys are created with scoped permissions: | Permission | Grants access to | | --------------------------- | ---------------------------------------------------------------------- | | `markets.matching.read` | `GET /v1/matching-markets/sports` | | `markets.read` | `GET /v1/markets`, `GET /v1/categories`, `GET /v1/markets/{market_id}` | | `events.read` | `GET /v1/events/{event_id}` | | `crypto.prices.read` | `GET /v1/crypto-prices/binance` | | `polymarket.wallet.read` | `GET /v1/polymarket/wallet` | | `polymarket.positions.read` | `GET /v1/polymarket/wallet/positions` | ## Key management [#key-management] You can manage your keys from the dashboard at [app.predictorsdk.com](https://app.predictorsdk.com). Create, revoke, or reroll keys directly from the API Keys page. ## Rate limits [#rate-limits] If a key hits an enforced limit, the API returns `429` with `Retry-After` in seconds. Authenticated responses also include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`; reset is a Unix timestamp in seconds. `X-RateLimit-Limit` is your plan's `rate_limit_per_min` over a rolling 60 seconds, and it is the number to pace against. `X-RateLimit-Remaining` is advisory only — the budget recovers continuously rather than refilling at `X-RateLimit-Reset`, so it can repeat or even increase while you are spending it. See [Error Handling](/guides/error-handling#building-a-local-limiter) for how to build a limiter that works, and for retry guidance. # Events and Markets (/concepts/events-and-markets) PredictorSDK matches prediction markets across sources into canonical events. This page explains the internal data model that powers matching. The public API returns a simplified view -- see the [API Reference](/api-reference) for the exact response shape. ## Events [#events] An event represents a single real-world occurrence, like an NBA game. Internally, each event gets a stable canonical identifier. For team sports the ID follows the pattern `---`, e.g. `nba-okc-sas-2026-10-20`. ## Sub-markets [#sub-markets] Each event contains sub-markets. A sub-market is a specific bet type within the event, identified by a canonical key. ``` spread|full|spread:hou|8.5 ``` Key fields in the internal model: | Field | Description | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `key` | Canonical sub-market identifier within the event | | `market_type` | `moneyline`, `spread`, `total`, or `player_prop` | | `segment` | `full`, `half:1`, `quarter:1`, etc. | | `line` | Numeric line value when applicable. Omitted on moneylines, signed for spreads, positive for totals and props | | `subject` | Optional market subject. Omitted for event-owned game lines such as moneyline and total | | `rules` | Optional settlement metadata when it materially affects matching | For event-owned game lines, `subject` is omitted entirely. Moneyline rows also omit `line`, while full-game totals may include `rules.settlement` when overtime treatment affects matching. ## Outcomes [#outcomes] Each sub-market defines canonical outcomes that source-native selections map into. For example, a spread market has `cover` and `not_cover` outcomes. ## Source matching [#source-matching] The sports matching engine maps source-native markets from Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade, and ProphetX into these canonical structures. AlphaArcade participates for full-game moneylines in matcher-supported leagues when its structured start date agrees with any explicit date in its title or slug (MLB and WNBA are live today). ProphetX participates for full-game moneylines in NFL, MLB, NBA, and NHL, resolving each side through the event's structured competitor list. Verified NFL player props also participate through [same-prop comparisons with settlement-rule differences](/guides/player-prop-matching). Hyperliquid is currently exposed as provider-native passthrough markets in the market catalog and detail/event endpoints, not as matched sports rows. The public API exposes matched sports results as a flat map of platform markets grouped by source identifier, with an opt-in canonical submarket identity view -- see [Matching Markets](/concepts/matching-markets) for details on how matching works. ## Public API response [#public-api-response] The `GET /v1/matching-markets/sports` endpoint returns a `markets` map keyed by source identifier. Each value is an array of `PlatformMarket` objects showing which platforms have matching markets for that event: ```json { "markets": { "KXWNBAGAME-26AUG28TORLV": [ { "platform": "KALSHI", "event_id": "KXWNBAGAME-26AUG28TORLV", "event_ticker": "KXWNBAGAME-26AUG28TORLV", "market_tickers": ["KXWNBAGAME-26AUG28TORLV-LV"] }, { "platform": "POLYMARKET", "event_id": "wnba-tor-las-2026-08-28", "market_slug": "wnba-tor-las-2026-08-28", "token_ids": ["1661455139..."] } ] } } ``` `event_id` is the unified handoff to `GET /v1/events/{event_id}`. Its value remains provider-native: a Kalshi event ticker, Polymarket event slug, Predict market ID, SX Bet fixture ID, or AlphaArcade parent ULID. The existing market-selection fields remain available for market-level lookups. Always send the row's own `platform` alongside it — the events endpoint matches that value case-insensitively, so you can pass it through verbatim: ``` GET /v1/events/{row.event_id}?platform={row.platform} ``` The composite `{platform}:{id}` form that `GET /v1/markets` returns in `data[].id` is accepted here too, so `GET /v1/events/predict:1607914` is equivalent and equally unambiguous. Omitting `platform` is safe only for Kalshi and SX Bet, whose identifier shapes route on their own. A Predict market ID is a bare number and a Polymarket identifier can be a bare number or a kebab-case slug — the shapes are shared, and the two id spaces really do overlap. Sampled 2026-08-25, 53 of 93 Predict market IDs taken from `/v1/matching-markets/sports` also named a real Polymarket market. When you omit `platform` on one of those shared shapes, the endpoint probes both providers and answers only if exactly one of them holds that identifier. If both do, it returns **`409 Conflict`** listing the candidates rather than picking one: ```json { "error": "ambiguous event_id", "message": "event_id \"1607914\" resolves on polymarket and predict, which are different events. Retry with ?platform= naming the one you want, or use the composite {platform}:{id} form.", "candidates": ["polymarket", "predict"], "status_code": 409 } ``` Passing `platform` on every request is the one rule that works for all six providers. It is also faster and more available: the probe has to reach both providers to prove there is no collision, so it fails when either is having an outage, while a named platform depends only on that one. An AlphaArcade ULID and a Hyperliquid integer id are not inferred at all and always require `platform` (or the composite form). See the [TypeScript SDK Reference](/sdk/typescript) for the full type definitions. Pass `include_submarkets=true` when you need exact game-line identity instead of only the compact event-level bridge. Use `event_id=` to retrieve a known event without scanning list pages; any one lookup filter accepts up to 100 unique identifiers. The response always adds a `canonical_events` map when opted in, including an empty map when nothing matches. Each event contains canonical participants and submarkets with `market_type`, `segment`, `line`, outcomes, and exact provider-native market/outcome references. Spread and player-prop rows also include the public `subject` that gives the signed line meaning; event-owned moneyline and total rows omit it: ```json { "canonical_events": { "mlb-tor-cle-2026-09-01": { "event_id": "mlb-tor-cle-2026-09-01", "sport": "baseball", "league": "mlb", "title": "Toronto Blue Jays vs. Cleveland Guardians", "submarkets": [ { "key": "spread|full|spread:cle|2.5", "market_type": "spread", "segment": "full", "display_name": "Cleveland Guardians -2.5", "line": -2.5, "subject": { "type": "participant", "key": "cle", "name": "Cleveland Guardians" }, "outcomes": [ { "key": "cover:cle", "label": "Cleveland Guardians covers -2.5", "side": "cover" }, { "key": "not_cover:cle", "label": "Cleveland Guardians does not cover -2.5", "side": "not_cover" } ], "source_markets": [ { "provider": "polymarket", "market_id": "4038830", "market_name": "Spread: Cleveland Guardians (-2.5)", "outcomes": [ { "canonical_outcome_key": "cover:cle", "label": "Cleveland Guardians", "outcome_id": "3755469917..." } ] } ] } ] } } } ``` `outcome_id` is a source-native selection reference, never a universal cross-provider outcome ID. It is the same string [`GET /v1/markets/{market_id}`](/api-reference/getMarket) returns as `outcomes[].outcome_id` for the market this row names, on every provider. Polymarket, Predict and AlphaArcade publish a globally unique per-outcome token. SX Bet, Kalshi and ProphetX publish none, so their references are market-scoped and only mean something read together with `market_id`: SX Bet exposes one market hash plus two named positions (`outcomeOne`, `outcomeTwo`), a Kalshi market is binary (`yes`, `no`), and ProphetX numbers the sides of a market with small integers (`4`, `5`) that repeat on every moneyline. A provider can appear more than once under one submarket when it models the canonical market as several native ones — Kalshi lists a game moneyline as one binary market per team, so it contributes one entry per team ticker. This remains an identity endpoint. Use the returned IDs with market detail for current status, normalized quotes, timestamps, and liquidity. Consumers that require raw executable depth must obtain it from a venue or execution client; the matching response does not embed execution data. # Matching Markets (/concepts/matching-markets) The core abstraction in PredictorSDK is the **matching market**. When multiple prediction market platforms list the same real-world event, PredictorSDK groups them into a single canonical event with matched sub-markets. ## The problem [#the-problem] Each prediction market platform has its own way of representing markets: * **Kalshi** uses ticker symbols like `KXNBAGAME-26OCT20OKCSAS` * **Polymarket** uses Gamma market IDs/slugs for markets and CLOB token IDs for selections * **SX.Bet** uses on-chain market IDs * **Predict** uses numeric IDs like `77071` * **AlphaArcade** uses ULIDs and requires the `alpha-arcade` platform hint for event/detail lookups * **ProphetX** uses integer event ids and `:` market keys, and requires the `prophetx` platform hint (or the `prophetx:` composite prefix) for event/detail lookups The same game (Thunder at Spurs) appears differently on each platform. PredictorSDK resolves this by matching them into a single canonical event. ## How matching works [#how-matching-works] Player props distinguish exact prop identity from settlement equivalence. Strict matching is the default; opt into `player_prop_match=same_prop` with `include_submarkets=true` for a source-by-source rule matrix that also includes different or unverified rules. See [player-prop matching](/guides/player-prop-matching) for coverage, examples, and the complete options matrix. 1. **Event identification** — markets from different sources are grouped by the underlying real-world event (e.g., a specific NBA game on a specific date) 2. **Participant normalization** — team names, player names, and roles are canonicalized across sources 3. **Sub-market grouping** — within each event, markets are grouped by type (moneyline, spread, total, player prop), segment (full game, first half), and line value 4. **Outcome mapping** — source-native selections (yes/no, team names, over/under) are mapped to canonical outcome keys ## Stable identifiers [#stable-identifiers] Every matched entity gets a stable canonical identifier: * **Event ID**: `nba-okc-sas-2026-10-20` (league-away-home-date) * **Sub-market key**: `moneyline|full|winner` or `spread|full|spread:okc|8.5` * **Outcome key**: `winner:sas`, `cover:okc`, `over`, `under` These identifiers are deterministic and consistent across API calls. Pass a known event key through the repeatable `event_id` lookup filter (up to 100 unique values) to retrieve it without paginating list mode. Add `include_submarkets=true` for the normalized `canonical_events` identity map; the default compact `markets` bridge remains unchanged. Each platform row in that compact bridge also carries a provider-native `event_id`. Pass it to `GET /v1/events/{event_id}` to expand from a matched event to that provider's markets. This field is different from the canonical `event_id` lookup filter above: the filter accepts PredictorSDK's stable cross-provider key, while the nested field is deliberately native to one provider. Send the row's `platform` value as the events endpoint's `platform` query parameter every time — it is required for Predict market IDs, AlphaArcade ULIDs, and ProphetX ids. Without a platform, shared identifier shapes are probed across eligible providers: an identifier that resolves on more than one returns `409 Conflict` with candidates instead of silently choosing the wrong event. ## Identity, not odds [#identity-not-odds] PredictorSDK's matching surface focuses on **market identity and matching**, not odds or liquidity data. It tells you which markets exist, where they exist, and how they map to each other. Use the exact provider-native market/outcome references with market detail or the venue directly for current data. # Sources (/concepts/sources) PredictorSDK currently aggregates markets from seven prediction market platforms. Each platform's **source key** below is the value it carries in a market's `provider` field, and the value [`GET /v1/markets?provider=`](/guides/filtering#market-providers) takes to return only that platform's markets. ## Kalshi [#kalshi] A CFTC-regulated exchange for event contracts. Markets use ticker symbols (e.g., `KXNBAGAME-26OCT20OKCSAS`) and binary yes/no positions. Kalshi covers moneylines, spreads, totals, and player props for major sports. The catalog deliberately excludes Kalshi's multivariate-event markets (`KXMVECROSSCATEGORY…` tickers). These are user-built parlays: each distinct combination of legs someone assembles on Kalshi is minted as its own market, every leg is already listed individually, and they have no counterpart on any other venue. They made up 97% of Kalshi's open markets when the exclusion was introduced. `GET /v1/markets` and `pagination.total` describe Kalshi's ordinary markets only; a parlay ticker still resolves on `GET /v1/markets/{market_id}`, which fetches Kalshi live. Kalshi spells one NFL team differently from the canonical codes: its tickers use `JAC` for Jacksonville (canonical `jax`). Matching normalizes this, so `KXNFLGAME-26SEP13CLEJAC` resolves to the canonical event `nfl-cle-jax-2026-09-13`. **Source key:** `kalshi` ## Polymarket [#polymarket] A decentralized prediction market on Polygon. PredictorSDK identifies events and markets with Polymarket's Gamma IDs/slugs and identifies tradeable selections with CLOB token IDs; condition IDs are exposed where the source provides them. Polymarket covers moneylines, spreads, totals, and player props. Two Polymarket NFL conventions are normalized during matching. It spells the Los Angeles Rams as a bare `la` (the Chargers are `lac`), and **it dates NFL game slugs by the UTC date of kickoff rather than the Eastern one** — so `nfl-det-buf-2026-09-18` is a game that kicks off at 20:15 ET on September 17 and carries the canonical event ID `nfl-det-buf-2026-09-17`. Every prime-time NFL game is affected. Match by canonical `event_id` rather than by parsing a provider slug. **Source key:** `polymarket` ## Predict [#predict] A prediction market platform with numeric market IDs. Coverage includes moneylines for major sports, plus spreads and totals for NFL. Predict dates NFL game slugs by the UTC date of kickoff, the same convention Polymarket uses for NFL and the opposite of what both use for MLB. Matching resolves the game day from the category's real start time instead, so Predict's `nfl-sea-dal-2026-08-16` and Kalshi's Eastern-dated ticker land on one canonical event. **Source key:** `predict` ## SX.Bet [#sxbet] A blockchain-based sports betting exchange. Markets use on-chain IDs and cover moneylines, spreads, and totals. **Source key:** `sxbet` ## Hyperliquid [#hyperliquid] A decentralized exchange with HIP-4 outcome markets. Hyperliquid markets are available through `GET /v1/markets`, `GET /v1/markets/{market_id}`, and `GET /v1/events/{event_id}` as provider-native passthrough markets. **Source key:** `hyperliquid` ## AlphaArcade [#alphaarcade] A prediction market settled on Algorand. Markets are binary (Yes/No) or multi-choice (an `options[]` array, each option its own submarket), and are available through `GET /v1/markets`, `GET /v1/markets/{market_id}`, and `GET /v1/events/{event_id}`. AlphaArcade's full-game moneylines are additionally matched into `GET /v1/matching-markets/sports` for every league the matcher supports when its structured start date agrees with any explicit date in its display fields; contradictory rows remain available in the provider-native catalog but are not used for identity matching. Everything else is exposed as provider-native passthrough markets. **Source key:** `alpha-arcade` ## ProphetX [#prophetx] A sports betting exchange (Market Maker API). Events are bare integer ids and markets carry a per-event type id, so the stable market key is `:` and v1 routing is composite-only (`prophetx:`, `prophetx::`) or `?platform=prophetx`. Markets are available through `GET /v1/markets`, `GET /v1/markets/{market_id}`, and `GET /v1/events/{event_id}`. ProphetX's full-game moneylines are additionally matched into `GET /v1/matching-markets/sports` for NFL, MLB, NBA, and NHL; verified NFL player props also join same-prop comparisons with explicit settlement-rule differences. Spreads, totals, halves, and unverified props remain passthrough. Reads are authenticated (session tokens, one shared session per process) and paced to the keypair limit. **Source key:** `prophetx` ## Matched Sports Coverage [#matched-sports-coverage] Hyperliquid is not currently matched into `GET /v1/matching-markets/sports`; it is exposed through the market catalog and detail/event endpoints. AlphaArcade is matched for full-game moneylines only (MLB, WNBA and NFL live today) — its spread and total markets model each line as an option of a single parent market and are not matched, and its first-half and team-total markets are sub-markets of the game line and are never treated as the moneyline — and its other markets are exposed through the catalog and detail/event endpoints. Matched leagues are NBA, WNBA, NHL, MLB, and NFL. One league-specific gap is worth knowing about: **Predict lists no WNBA games**, so WNBA events match across Kalshi, Polymarket, SX.Bet, and AlphaArcade rather than all five sources. **NFL is mapped for Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade (moneyline), and ProphetX (moneyline and verified props)**, with a matching scope of full-game moneyline, spread, and total plus [verified player props with rule comparisons](/guides/player-prop-matching). NFL half and quarter lines remain outside matching. Predict lists NFL only in season, and AlphaArcade lists NFL game lines for most but not all games of a week (Week 1 of 2026: 14 of 16), so an NFL event carries the venues that actually list it. Games appear once at least two venues list them; Kalshi and SX.Bet typically open a week's games a week or two ahead, Polymarket several weeks ahead. | Market Type | Kalshi | Polymarket | Predict | SX.Bet | AlphaArcade | ProphetX | | ----------------------------------- | ------ | ---------- | ------- | ------ | ----------- | -------- | | Moneyline (full) | Yes | Yes | Yes | Yes | Yes | Yes | | Moneyline (1H, NBA/WNBA) | Yes | Yes | No | Yes | No | No | | Spread (full) | Yes | Yes | No | Yes | No | No | | Spread (1H, NBA/WNBA) | Yes | Yes | No | Yes | No | No | | Total (full) | Yes | Yes | No | Yes | No | No | | Total (1H, NBA/WNBA) | Yes | Yes | No | Yes | No | No | | NFL player props (same-prop policy) | Yes | Yes | No | No | No | Yes | | Player Points | Yes | Yes | No | No | No | No | | Player Rebounds | Yes | Yes | No | No | No | No | | Player Assists | Yes | Yes | No | No | No | No | The table describes supported matching shapes, not a guarantee that every current slate has cross-provider overlap. NBA/WNBA can match half and quarter game lines; the full-game-only scope for NFL and MLB does not apply to those basketball leagues. All player-prop rows require verified identity and the selected settlement policy; supported statistics and available lines differ by provider. ## Filtering by platform [#filtering-by-platform] Use the platform-specific query parameters on `GET /v1/matching-markets/sports` to look up matches by source identifier: ```bash # Look up by Kalshi event ticker ?kalshi_event_ticker=KXNBAGAME-26OCT20OKCSAS # Look up by Polymarket market slug ?polymarket_market_slug=nfl-pit-ne-2026-09-20 # Look up by AlphaArcade market ULID or ProphetX IDs ?alpha_arcade_market_id=YOUR_MARKET_ULID ?prophetx_event_id=1700008782 ?prophetx_market_id=1700008782:219 ``` Replace `YOUR_MARKET_ULID` with `market_id` from a current `ALPHA-ARCADE` matching row; game IDs can disappear after the venue delists them. See [Filtering](/guides/filtering) for the full list of available parameters. # Error Handling (/guides/error-handling) When a request fails, the API returns an error response with an HTTP status code and a JSON body. ## Error response format [#error-response-format] ```json { "error": "missing api key", "status_code": 401 } ``` The `status_code` field mirrors the HTTP status for clients that want to read it from the response body. ## Status codes [#status-codes] | Code | Meaning | Common cause | | ----- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Bad Request | A query parameter the endpoint does not read, or an invalid value for one it does (e.g., combining multiple filter types) | | `401` | Unauthorized | Missing or invalid API key | | `402` | Payment Required | Free monthly allowance is exhausted, or a paid subscription is in a payment-recovery state — see [Plans & Limits](/guides/plans) | | `403` | Forbidden | API key lacks the required permission for this endpoint | | `404` | Not Found | A requested market/event does not exist on the selected or inferred platform, or the path matches no route | | `405` | Method Not Allowed | The path exists but not under the HTTP method you sent — read the `Allow` header | | `409` | Conflict | A bare `market_id` or `event_id` resolves on more than one platform, so no single one can honestly be returned — see [Events & Markets](/concepts/events-and-markets) | | `429` | Too Many Requests | API key or an upstream provider exceeded its rate limit | | `502` | Bad Gateway | PredictorSDK could not get a valid response from an upstream provider or API-key authorization dependency | | `503` | Service Unavailable | A reader, service dependency, or endpoint configuration is temporarily unavailable | ### Routing errors [#routing-errors] Three of these come from the router rather than from an endpoint. They can arrive on any path, and none needs a valid API key — the router answers before the key is read. * A path matching **no route** returns `404` with `{"error": "not found", "status_code": 404}`. That is a different body from an endpoint's own `404` (`market not found`, `event not found`), which means the identifier was looked up and missed. Check the path before you check the ID. * A path that exists but **not under the method you sent** returns `405` with `{"error": "method not allowed", "status_code": 405}`, an `Allow` header naming the methods that path does accept, and the same list in `message`. Every matching/markets/events reference endpoint here is a `GET` (`POST /v1/account/billing/portal` is the documented billing exception). * A request carrying a **query parameter the endpoint does not read** returns `400` with `{"error": "unknown query parameter", "status_code": 400}`. The `message` names the rejected key and lists the ones that endpoint accepts. All three carry the same `error` / `status_code` body as every other error here. Because none is attached to an operation, the generated SDKs raise their generic API error (carrying the status code and the response body) rather than one of the per-status typed errors below. #### Unknown query parameters are rejected, not ignored [#unknown-query-parameters-are-rejected-not-ignored] Every parameter you send has to be one the endpoint declares. This is deliberate and it applies to every route: a silently dropped parameter returns a full unfiltered response that *looks* filtered, which is harder to diagnose than an error. The concrete case is `?platform=` on [`GET /v1/markets`](/api-reference/getMarkets) — that is the spelling [`GET /v1/matching-markets/sports`](/api-reference/getSportsMatchingMarkets) uses for the same concept, so it is the natural guess, and it used to return the whole catalogue with a `200` on it. ``` GET /v1/markets?platform=sxbet 400 {"error": "unknown query parameter", "message": "This endpoint does not read the query parameter \"platform\". It accepts: category, cursor, limit, provider.", "status_code": 400} ``` The parameter to reach for on that endpoint is `provider` — see [Filtering](/guides/filtering). If you use a generated SDK you cannot hit this: its methods only expose the parameters each endpoint declares. ## Rate-limit headers [#rate-limit-headers] Authenticated responses include the current key's rate-limit state: | Header | Unit | Meaning | | ----------------------- | ------------ | ---------------------------------------------------------------------------------------------------- | | `X-RateLimit-Limit` | requests | Requests allowed in a 60-second window. Equals your plan's `rate_limit_per_min` from `GET /v1/plans` | | `X-RateLimit-Remaining` | requests | **Advisory estimate.** Not a countdown — see below | | `X-RateLimit-Reset` | Unix seconds | End of the current window. A step function: the same value on every response inside one window | | `Retry-After` | seconds | Whole seconds to wait; included on API-key `429` responses and optional on upstream `429` responses | Unauthenticated routes — `GET /v1/plans`, and the router-level `404` and `405` above — carry none of them. The limiter is only consulted once a key has been read. `X-RateLimit-Reset` is a Unix epoch in **seconds**, not milliseconds. The generated SDK retry layers accept both the documented seconds form and the legacy millisecond epoch that older deployments emitted. They cap retry delays at 60 seconds and retry `408`, `429`, and `5xx` responses. ### Building a local limiter [#building-a-local-limiter] **Do not pace from `X-RateLimit-Remaining`.** The limiter's budget recovers continuously as earlier requests age out rather than refilling all at once, so the value legitimately moves in both directions: between two strictly sequential responses inside one `X-RateLimit-Reset` window it may repeat, skip values, or increase. Measured on 2026-08-26 against a 1000 req/min key, 120 sequential requests over three windows: **42 increases, the largest +8**. It also runs ahead of your consumption — by the end of a fast burst it over-stated the true remaining budget by up to **44** requests — and the gap widens the faster you send, so it is least accurate exactly when you would consult it. Treat a low value as a reason to slow down; never treat a high one as permission to burst. Two consequences worth stating outright, because both contradict the usual fixed-window mental model: * `X-RateLimit-Reset` is **not a refill instant**. Nothing jumps back to `X-RateLimit-Limit` when it passes; four boundary crossings measured on the same day moved the reported budget by +5, 0, +2 and +2. Use it as an upper bound on how long a `429` can last. * Consuming exactly `X-RateLimit-Limit` requests and then waiting for `X-RateLimit-Reset` is therefore not the contract. Spread the same budget over a rolling 60 seconds instead. What does work is a client-side sliding window, which needs nothing from `X-RateLimit-Remaining` at all: 1. Read `rate_limit_per_min` for your plan from `GET /v1/plans`, or take `X-RateLimit-Limit` from any authenticated response. They are the same number. 2. Keep your own timestamps for the last 60 seconds of requests and refuse to send when that count reaches the limit — or simply space requests `60 / limit` seconds apart, which is the same thing without the bookkeeping. 3. Back off whenever a `429` arrives, honouring `Retry-After`. The `429` and `Retry-After` are authoritative; they are computed from the limiter's own window instant and never disagree with `X-RateLimit-Reset`. ### `402 Payment Required` response [#402-payment-required-response] The response body includes an `action` field so clients can render the right follow-up: | `action` | Meaning | Recommended UX | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `upgrade_plan` | The caller is on a lower tier than the endpoint requires, or a Free caller exhausted the monthly request allowance. | Route them to `/pricing` or a plan-upgrade CTA. | | `resolve_payment` | The caller *had* a paid subscription, but it's now in a payment-recovery state (`past_due`, `unpaid`, `paused`, `incomplete`). The backend has already downgraded them to Free. | Route them to a signed-in billing/settings flow that can call `POST /v1/account/billing/portal`; that endpoint requires an authenticated account session, not the API key that received the `402`. | Both variants return the same `402` status and include `required_tier` / `current_tier` so clients can keep a single handler with an `action` switch inside. When a Free caller exhausts the monthly allowance, the response also includes `included_requests_per_month` and `current_period_requests`. ## Handling errors in the SDK [#handling-errors-in-the-sdk] All SDKs throw typed errors for known HTTP status codes — including `PaymentRequiredError` for `402`, which exposes the `action`, `required_tier`, `current_tier`, and (on Free-cap 402s) `included_requests_per_month` + `current_period_requests` fields directly on the error body. ```ts import { PredictorSDK, PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); try { await client.getSportsMatchingMarkets({ kalshiEventTicker: "KXWNBAGAME-26AUG28TORLV", polymarketMarketSlug: "wnba-tor-las-2026-08-28", }); } catch (error) { if (error instanceof PredictorSDK.BadRequestError) { console.log("Bad request:", error.body.error); } else if (error instanceof PredictorSDK.PaymentRequiredError) { if (error.body.action === "resolve_payment") { // Subscription in payment recovery — send to billing portal } else { // Plan or allowance ceiling — send to /pricing } } else if (error instanceof PredictorSDK.TooManyRequestsError) { // Rate limited — back off and retry } else if (error instanceof PredictorSDK.UnauthorizedError) { // Invalid API key } } ``` ```python from predictorsdk import PredictorSDK, BadRequestError, PaymentRequiredError client = PredictorSDK(token="YOUR_API_KEY") try: client.get_sports_matching_markets( kalshi_event_ticker="KXWNBAGAME-26AUG28TORLV", polymarket_market_slug="wnba-tor-las-2026-08-28", ) except BadRequestError as e: print(f"Bad request: {e.body.error}") except PaymentRequiredError as e: if e.body.action == "resolve_payment": # Subscription in payment recovery — send to billing portal ... else: # Plan or allowance ceiling — send to /pricing ... ``` ```go import ( "context" "errors" "fmt" predictorclient "github.com/PredictorSDK/sdk-go/client" predictorsdk "github.com/PredictorSDK/sdk-go" "github.com/PredictorSDK/sdk-go/option" ) func main() { client := predictorclient.New(option.WithToken("YOUR_API_KEY")) _, err := client.GetSportsMatchingMarkets(context.TODO(), &predictorsdk.GetSportsMatchingMarketsRequest{ KalshiEventTicker: []*string{predictorsdk.String("KXWNBAGAME-26AUG28TORLV")}, PolymarketMarketSlug: []*string{predictorsdk.String("wnba-tor-las-2026-08-28")}, }) var badReq *predictorsdk.BadRequestError var payReq *predictorsdk.PaymentRequiredError switch { case errors.As(err, &badReq): fmt.Println("Bad request:", badReq.Body.Error) case errors.As(err, &payReq): if payReq.Body.Action != nil && *payReq.Body.Action == predictorsdk.PaymentRequiredErrorActionResolvePayment { // Subscription in payment recovery — send to billing portal } else { // Plan or allowance ceiling — send to /pricing } } } ``` ## Retry guidance [#retry-guidance] * **429** — honor `Retry-After`; otherwise calculate the delay from the second-based `X-RateLimit-Reset`. Add jitter only when coordinating many clients. Do not decide whether to send from `X-RateLimit-Remaining` — see [Building a local limiter](#building-a-local-limiter). * **502/503** — transient dependency failures. Retry with bounded exponential backoff; stop at your application's latency budget. * **404** — do not retry unchanged. Check the ID or pass `platform` explicitly when an identifier shape is ambiguous. If the body reads `not found` rather than `market not found` / `event not found`, the path itself is wrong. * **405** — do not retry unchanged. Re-send with a method from the `Allow` header. * **409** — do not retry unchanged. The identifier is genuinely ambiguous, so the same request conflicts again. Re-send with `?platform=` naming one of the `candidates` in the body, or with the composite `{platform}:{id}` form. * **400/401/402/403** — do not retry. Fix the request, plan, permission, or credentials. # Filtering (/guides/filtering) The `GET /v1/matching-markets/sports` endpoint accepts optional lookup filters. When called without any parameters it returns all currently matched sports markets. The unified market catalog supports a provider filter and a canonical top-level category filter. ## Available filters [#available-filters] For canonical player props, `include_submarkets=true` uses strict settlement equivalence by default. Add `player_prop_match=same_prop` to include verified same-prop identities with different or unverified rules. This policy works with every lookup filter below, list pagination, and `include_settled`; it does not change game-line matching. See the [options and rule matrix](/guides/player-prop-matching). | Parameter | Type | Description | Example | | ------------------------ | ------------------- | ------------------------------------------------------------------------------- | ------------------------- | | `event_id` | string (repeatable) | Look up matches by canonical event key(s) | `nba-okc-sas-2026-10-20` | | `kalshi_event_ticker` | string (repeatable) | Look up matches by Kalshi event ticker(s) | `KXNBAGAME-26OCT20OKCSAS` | | `polymarket_market_slug` | string (repeatable) | Look up matches by Polymarket market slug(s) | `nfl-pit-ne-2026-09-20` | | `predict_market_id` | string (repeatable) | Look up matches by Predict market ID(s) | `1607914` | | `sxbet_market_id` | string (repeatable) | Look up matches by SX Bet market ID(s) | `0xb4d047a7...` | | `alpha_arcade_market_id` | string (repeatable) | Look up matches by AlphaArcade market ULID(s) from a current `ALPHA-ARCADE` row | `YOUR_MARKET_ULID` | | `prophetx_event_id` | string (repeatable) | Look up matches by ProphetX event ID(s) | `1700008782` | | `prophetx_market_id` | string (repeatable) | Look up matches by ProphetX market ID(s) (`:`) | `1700008782:219` | Each filter accepts up to 100 unique values. Repeat the same parameter name for multiple values; do not comma-join them. Only one filter type may be used per request: for example, `event_id` cannot be combined with `polymarket_market_slug`. `limit` and `cursor` are ignored in lookup mode, which returns all requested matches in one bounded, unpaginated response. ## Market categories [#market-categories] Use `GET /v1/categories` to list the canonical category values, then pass one to `GET /v1/markets`: ```bash GET /v1/markets?category=sports GET /v1/markets?category=crypto ``` Only the canonical category values are accepted (case-insensitive). Any other value returns `400`, so use the list from `GET /v1/categories` rather than provider-native tags like `mlb` or `btc`. The category filter is part of pagination scope, so keep the same `category` value when following a `next_cursor`. ## Market providers [#market-providers] `GET /v1/markets` walks providers in a fixed order — `kalshi`, `polymarket`, `predict`, `sxbet`, `hyperliquid`, `alpha-arcade`, `prophetx` — so without a filter the first pages are all Kalshi, and reaching a later provider means paginating through every earlier one. Pass `provider` instead: ```bash GET /v1/markets?provider=sxbet GET /v1/markets?provider=alpha-arcade ``` The value is each row's own `provider` field, so it is the same token [Sources](/concepts/sources) lists as that platform's source key. Only those seven values are accepted (case-insensitive). Any other value returns `400` listing the legal ones — including spellings the `platform` override on `GET /v1/markets/{market_id}` tolerates, such as `sx_bet`, `hl`, or `px`: ```json { "error": "invalid provider: must be one of kalshi, polymarket, predict, sxbet, hyperliquid, alpha-arcade, prophetx", "status_code": 400 } ``` `pagination.total` counts only the selected provider's rows, not the whole catalog, and the filter is part of pagination scope — keep the same `provider` value when following a `next_cursor`. `snapshot.observed_at` narrows with it. Unfiltered it is the oldest provider crawl behind the page — a floor for the whole catalog — while under `provider` it is that provider's own read time. Filtering is therefore the cheapest way to get an exact freshness bound instead of a conservative one; see [Pagination](/guides/pagination). Both filters compose, and the result is the intersection: ```bash GET /v1/markets?provider=polymarket&category=sports ``` `provider` on `GET /v1/markets` filters a list. The `platform` parameter on `GET /v1/markets/{market_id}` and `GET /v1/events/{event_id}` is a different thing: it names the venue a single identifier should be resolved against. ## Examples [#examples] ### Look up a canonical event directly [#look-up-a-canonical-event-directly] Use this path when you already hold the stable event key; there is no need to paginate list mode to rediscover it. ```bash GET /v1/matching-markets/sports?event_id=nba-okc-sas-2026-10-20 ``` ### Look up a single Kalshi event [#look-up-a-single-kalshi-event] ```bash ?kalshi_event_ticker=KXNBAGAME-26OCT20OKCSAS ``` ### Look up multiple Kalshi events [#look-up-multiple-kalshi-events] Repeat the parameter for each ticker: ```bash ?kalshi_event_ticker=KXNBAGAME-26OCT20OKCSAS&kalshi_event_ticker=KXNBAGAME-26OCT20BOSDET ``` The same repeatable-value form applies to every lookup filter, including `event_id`. ### Look up by Polymarket slug [#look-up-by-polymarket-slug] ```bash ?polymarket_market_slug=nfl-pit-ne-2026-09-20 ``` ### Look up by Predict market ID [#look-up-by-predict-market-id] ```bash ?predict_market_id=1607914 ``` ### Look up by SX Bet market ID [#look-up-by-sx-bet-market-id] ```bash ?sxbet_market_id=0xb4d047a709aae881e5ccad9d123592967644ee1df1f17078c762b388e41b81c5 ``` ### Look up by AlphaArcade market ULID [#look-up-by-alphaarcade-market-ulid] First list current matches and copy `market_id` from an `ALPHA-ARCADE` row. Replace the placeholder below with that value. A game's ULID can disappear when AlphaArcade delists the game; it is not a durable example identifier. ```bash ?alpha_arcade_market_id=YOUR_MARKET_ULID ``` ### Look up by ProphetX event or market ID [#look-up-by-prophetx-event-or-market-id] ```bash ?prophetx_event_id=1700008782 ?prophetx_market_id=1700008782:219 ``` ### Return all matched markets [#return-all-matched-markets] ```bash GET /v1/matching-markets/sports ``` No parameters required. Returns the first page of currently matched sports events with its cross-platform identifiers. See [Pagination](/guides/pagination) for how to traverse larger result sets. ### Include settled events [#include-settled-events] ```bash ?include_settled=true ``` `include_settled` picks which events the request draws from, and it applies to lookups as well as to list mode. By default you get only events whose scheduled start has not certainly passed — today's games, plus a one-day grace so a late start that runs past midnight Eastern is never dropped while it is still being played. Set `include_settled=true` to also get events whose game date is further in the past. The date comes from the game, not from the venue. A prediction market can keep a finished game quoted and open for months, so an event that is missing from the default page has not necessarily been resolved upstream — it just is not current. If you are holding a canonical `event_id` for a game that has already been played, ask for it with `include_settled=true`; without the flag the lookup answers `200` with an empty `markets` object. This works alongside pagination, but `include_settled` is part of cursor scope, so changing it mid-traversal requires starting over from page 1. ### Include canonical submarket identity [#include-canonical-submarket-identity] ```bash GET /v1/matching-markets/sports?event_id=nba-okc-sas-2026-10-20&include_submarkets=true ``` `include_submarkets=true` adds a `canonical_events` map without changing the default `markets` map. It contains canonical event/participant identities, normalized market type, segment, signed spread or positive total/prop `line`, settlement rules where material, canonical outcomes, and exact provider-native market/outcome references. It is deliberately identity-only: it does not add prices, status, timestamps, liquidity, fees, or order-book depth. Join the returned exact IDs to market detail or a venue client for those values. When the opt-in is set and nothing matches, `canonical_events` is an empty map. ## Response keys [#response-keys] The response `markets` object is keyed by the identifier you queried with: * When using `kalshi_event_ticker`, the key is the Kalshi ticker. * When using `polymarket_market_slug`, the key is the Polymarket slug. * When using `alpha_arcade_market_id`, `prophetx_event_id`, or `prophetx_market_id`, the key is the requested ID with surrounding whitespace trimmed. Identifier case is preserved and must match the source. * When using `event_id`, the key is the requested canonical event ID. * When no filter is provided, the key is the canonical event ID (e.g., `nba-okc-sas-2026-10-20`). Do not confuse that canonical key with `markets[...][].event_id`. The nested field is a provider-native identifier intended for `GET /v1/events/{event_id}`; the `event_id` query parameter in this guide is a PredictorSDK canonical lookup key. ## Filter vs. list mode [#filter-vs-list-mode] The endpoint has two modes: * **Lookup mode** — one lookup filter is set (`event_id`, `kalshi_event_ticker`, `polymarket_market_slug`, `predict_market_id`, `sxbet_market_id`, `alpha_arcade_market_id`, `prophetx_event_id`, or `prophetx_market_id`). Response omits the `pagination` block; the result is bounded by the requested IDs (100 unique values maximum). * **List mode** — no filter set. Response includes a `pagination` block and respects `limit`/`cursor`. Default limit is 25 (max 100). # Pagination (/guides/pagination) List endpoints that can return many results use **cursor-based pagination**. The cursor is opaque — you don't need to parse it; just pass the `next_cursor` from one response back as `cursor` on the next request. ## Endpoints that paginate [#endpoints-that-paginate] * `GET /v1/matching-markets/sports` — list mode only (no platform-ID filter). Default `limit=25`, max `100`. * `GET /v1/markets` — all requests are paginated, including `?provider=` and `?category=` filters. Default `limit=25`, max `100`. Lookup-mode calls on matching-markets (canonical `event_id` or a platform-ID filter set) do not paginate — the response is bounded by at most 100 unique IDs of one filter type and omits the `pagination` block entirely. ## Response shape [#response-shape] Every paginated response includes a `pagination` block alongside its data payload. The data field name varies by endpoint (`markets` for matching-markets, `data` for `/v1/markets`), but the pagination block is always the same: ```json { "pagination": { "limit": 25, "total": 143, "has_more": true, "next_cursor": "eyJzIjoiYWJjMTIzZWYi..." } } ``` | Field | Description | | ------------- | -------------------------------------------------------------------------------------------- | | `limit` | Echoes the `limit` query param (or the default). | | `total` | Total matching items across all pages. | | `has_more` | `true` when more pages exist beyond this one. | | `next_cursor` | Pass back via the `cursor` query param for the next page. Absent when `has_more` is `false`. | ## Walking every page [#walking-every-page] ```bash # Page 1 curl -H "Authorization: Bearer $API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports?limit=25" # Page 2 (use next_cursor from page 1's response) curl -H "Authorization: Bearer $API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports?limit=25&cursor=eyJzIjoi..." ``` ```ts const allEvents: Record = {}; let cursor: string | undefined; do { const page = await client.getSportsMatchingMarkets({ limit: 50, cursor }); Object.assign(allEvents, page.markets); cursor = page.pagination?.nextCursor; } while (cursor); ``` ```python all_events = {} cursor = None while True: page = client.get_sports_matching_markets(limit=50, cursor=cursor) all_events.update(page.markets) if not page.pagination or not page.pagination.has_more: break cursor = page.pagination.next_cursor ``` ```go allEvents := map[string][]*predictorsdk.PlatformMarket{} var cursor *string for { page, err := client.GetSportsMatchingMarkets(ctx, &predictorsdk.GetSportsMatchingMarketsRequest{ Limit: predictorsdk.Int(50), Cursor: cursor, }) if err != nil { return err } for k, v := range page.Markets { allEvents[k] = v } if page.Pagination == nil || !page.Pagination.HasMore { break } cursor = page.Pagination.NextCursor } ``` ## Important rules [#important-rules] ### Keep request shape constant across pages [#keep-request-shape-constant-across-pages] The cursor is scoped to the request shape it was issued with. If you change `include_settled`, `provider`, `category`, or any other supported filter mid-traversal, you'll get a `400`: ```json { "error": "pagination cursor does not match the current filters" } ``` Drop the cursor and start fresh from page 1 whenever filters change. ### Market cursors bind to an immutable snapshot [#market-cursors-bind-to-an-immutable-snapshot] `GET /v1/markets` binds the first page to an immutable catalog manifest. A provider refresh can publish concurrently without changing what that cursor sees, so continuation returns the remaining records from the same snapshot and never advances past records that were not delivered. Replaced snapshots normally remain available for up to 8 hours. Under storage pressure, Valkey may evict a retained snapshot earlier; active snapshots are protected. In either case, the API rejects an unfinished old cursor with: ```json { "error": "pagination cursor is stale; restart from the first page" } ``` Restart from page 1 whenever a retained cursor goes stale. ### `snapshot.observed_at` tells you how old the page is [#snapshotobserved_at-tells-you-how-old-the-page-is] Because the endpoint reads a stored snapshot rather than calling the venues, the rows are as old as the last ingestion crawl — not as old as the request. Every `GET /v1/markets` response carries that age: ```json { "data": [ ], "snapshot": { "observed_at": "2026-08-25T13:41:22.108Z" }, "pagination": { "limit": 25, "total": 1393200, "has_more": true, "next_cursor": "..." } } ``` It sits beside `data` rather than inside `pagination` because it describes the data, not the page. Three things worth knowing: * **It is a floor.** Providers are crawled on independent schedules, so the catalog is a merge of snapshots of different ages, and this is the OLDEST of them. Every row is at least this fresh; most are fresher. * **`?provider=` makes it exact.** Under a provider filter the value is that provider's own read time, the same way `pagination.total` counts only the filtered set. That is the cheapest way to get a tight bound for the rows you actually care about. * **It does not move during a traversal.** The cursor is bound to one immutable snapshot, so every page of one traversal reports the identical value even while newer snapshots publish behind you. Start a fresh first page to see a newer one. `null` means the bound snapshot carries no read time; treat it as unknown age rather than as fresh. ### Cursors are ephemeral [#cursors-are-ephemeral] Treat cursors as short-lived session state — finish traversal promptly, never assume the full 8-hour grace is guaranteed under storage pressure, and don't share them between clients. Start a new first page when you want the latest provider snapshots. ## Default behavior [#default-behavior] * **No `limit`** → `limit=25`. * **No `cursor`** → first page. * **`limit > 100`** → `400 invalid limit`. * **Malformed `cursor`** → `400 invalid cursor`. * **Cursor with stale scope** → `400 pagination cursor does not match the current filters`. * **Unavailable retained `/v1/markets` snapshot (expired or evicted under pressure)** → `400 pagination cursor is stale; restart from the first page`. # Plans & Limits (/guides/plans) Pricing is published on [predictorsdk.com/pricing](https://predictorsdk.com/pricing). This page is the developer-facing summary: how throughput limits work, which tier each endpoint requires, and how to read plan data programmatically. For the machine-readable version, hit `GET /v1/plans` — no auth required. ## Plans at a glance [#plans-at-a-glance] | Plan | Monthly allowance | Rate limit | API keys | Overage | | ---------- | ----------------: | ----------: | -------: | ---------: | | Free | 1,000 / mo | 10 / min | 1 | None | | Starter | 100,000 / mo | 60 / min | 3 | $0.25 / 1K | | Pro | 500,000 / mo | 300 / min | 10 | $0.12 / 1K | | Business | 2,000,000 / mo | 1,000 / min | 25 | $0.06 / 1K | | Enterprise | Custom | Custom | Custom | Custom | * **Monthly allowance** applies per UTC calendar month. * **Rate limit** is a per-key rolling 60-second ceiling. Pace from the response's `Retry-After` and `X-RateLimit-Reset` headers rather than assuming a particular server-side algorithm. * **Overage** only applies to paid plans. Free is a hard cap (see below). ## Endpoint availability [#endpoint-availability] | Endpoint | Free | Starter | Pro | Business | Enterprise | | ------------------------------------- | :--: | :-----: | :-: | :------: | :--------: | | `GET /v1/plans` (unauthenticated) | Yes | Yes | Yes | Yes | Yes | | `GET /v1/matching-markets/sports` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/crypto-prices/binance` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/markets` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/categories` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/markets/{market_id}` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/events/{event_id}` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/polymarket/wallet` | Yes | Yes | Yes | Yes | Yes | | `GET /v1/polymarket/wallet/positions` | Yes | Yes | Yes | Yes | Yes | All three modes of the sports matching endpoint — lookup (with a platform-ID filter), list (no filter), and settled history (`include_settled=true`) — resolve to the same tier gate. See the [filtering](/guides/filtering) and [pagination](/guides/pagination) guides for how the modes differ in request shape and response. ## Free plan cap [#free-plan-cap] Free is the only tier with a hard monthly ceiling — there is no overage. The cap is **1,000 authenticated request attempts per UTC calendar month** on authenticated endpoints. "Attempt" means the call is counted before the handler runs, so failed or malformed requests still consume allowance. When the cap is reached, all authenticated calls return `402 Payment Required` with `action: upgrade_plan` for the rest of the month. The 402 response body includes `included_requests_per_month` and `current_period_requests` so clients can show remaining allowance without a separate call. See [Error Handling](/guides/error-handling) for the full 402 response shape. ## Reading plans programmatically [#reading-plans-programmatically] `GET /v1/plans` returns the full catalog — prices, allowances, features — as JSON. No auth required. This is what the pricing page renders from, so it stays in sync automatically. ```bash curl https://api.predictorsdk.com/v1/plans ``` The response shape: ```json { "data": [ { "key": "free", "name": "Free", "billing_tier": "free", "monthly_price_cents": 0, "included_requests_per_month": 1000, "overage_cents_per_1k": 0, "rate_limit_per_min": 10, "max_keys": 1, "features": ["Sports matching endpoint (lookup + list modes)", "..."], "contact_sales": false } ] } ``` Stripe price IDs are stripped from the public response. The generated clients expose the same unauthenticated operation: ```ts const catalog = await client.getPlans(); ``` Python uses `client.get_plans()` and Go uses `client.GetPlans(ctx)`. ## When you hit 402 [#when-you-hit-402] If a request comes back with `402`, read `required_tier` from the body and route the user to `/pricing` (or show an in-app upgrade prompt). The `action` field distinguishes "upgrade to a higher plan" from "your existing subscription needs a payment fix" — handle both with a single switch in your error handler. The [Error Handling guide](/guides/error-handling) has example code for each SDK. # Player-prop matching and settlement rules (/guides/player-prop-matching) Two markets can ask the same player to exceed the same line and still pay differently when that player does not play. PredictorSDK separates **prop identity** from **settlement equivalence**. These options apply to player props only; they do not certify the full settlement rules of moneylines, spreads, or game totals. ## Choose a policy [#choose-a-policy] Use the existing [sports matching endpoint](/api-reference/getSportsMatchingMarkets). | Request | Player props returned | | ------------------------------------------------------ | --------------------------------------------------------------------------------- | | No parameters | Compact full-game moneyline bridge; no canonical submarkets | | `include_submarkets=true` | Strict settlement-equivalent player props only | | `include_submarkets=true&player_prop_match=strict` | Same strict default, explicitly selected | | `include_submarkets=true&player_prop_match=same_prop` | Verified same-prop identities, including different or unverified settlement rules | | `player_prop_match` without `include_submarkets=true` | `400`; the requested comparison would not be visible | | An empty, repeated, or unsupported `player_prop_match` | `400`; no silent fallback | Both policies work with list pagination, every existing single-type lookup filter, and `include_settled=true`. A cursor belongs to one policy and cannot be reused after switching policies. Lookup still accepts at most 100 unique identifiers of one filter type and remains unpaginated. The compact `markets` map continues to describe full-game moneylines, not player-prop outcomes. ```http GET /v1/matching-markets/sports?include_submarkets=true&player_prop_match=same_prop ``` For a known event, add `event_id=`; do not search every page to rediscover it. A result requires at least two distinct providers, not two markets from the same venue. A prop-only event can have an empty compact moneyline array while its canonical event contains player props. ## What must match in both modes [#what-must-match-in-both-modes] The sport and league, both teams, game date, player, statistic, full-game period, threshold, and native selection meanings must agree. NFL kickoff dates are normalized to America/New\_York; Polymarket's UTC-dated slug is not used as the Eastern game date. Player names are resolved against both teams' ESPN rosters to an unambiguous athlete ID, and exactly one ESPN fixture must corroborate the matchup and Eastern game day. An explicit source kickoff that conflicts with ESPN is refused. Explicit full/display-name aliases and individually reviewed, athlete-bound spellings may match; initials, surname-only guesses, suffix removal, and arbitrary collision resolution do not. For integer statistics, **250+ means Over 249.5**, not Over 250. A contract requiring **more than 10 points** is not the same as **10+ points**. Only verified nonnegative half-point thresholds are admitted in this release; whole-number push contracts, exact-value/range bets, fractional statistics, weekly accumulation, season props, and D/ST units are not player-prop matches. Passing touchdowns and touchdowns scored are separate statistics. Roster checks are performed during sports ingestion, with a five-minute cache and bounded requests. A missing or stale roster, a team conflict, or an ambiguous athlete removes that prop, not ordinary game lines. Current rosters cannot certify historical membership: ingestion does not newly admit props more than 48 hours past their scheduled start. Previously verified archived props remain eligible under `include_settled=true`; older snapshots without identity verification do not acquire it retroactively. ## Read the rule matrix [#read-the-rule-matrix] Every returned player prop has `settlement_equivalence` and `rule_comparisons`. | Equivalence | Meaning | | ------------ | ------------------------------------------------------------------------ | | `equivalent` | Every source has a completely reviewed, equivalent settlement profile | | `different` | At least one known difference exists, even if other rules remain unknown | | `unverified` | No difference has been established, but equivalence has not been proved | Each `rule_comparisons` row has a machine-readable `rule`, readable `label`, `comparison`, and `source_values`. Each source value names its exact `provider` and `market_id`, a machine-readable `value`, a plain-language `description`, and an `evidence_url` when verified. The matrix always covers: | Rule | What to check | | ------------------- | ----------------------------------------------------------------- | | `non_participation` | Inactive, withdrawn, or no-play settlement; minimum participation | | `overtime` | Whether overtime statistics count | | `stat_definition` | Which statistic and attribution rules govern | | `threshold` | Comparison operator, equality, and push behavior | | `stat_corrections` | Whether later official corrections can change settlement | | `postponement` | How long a delayed game remains eligible | | `cancellation` | Fair-price settlement, split payout, or refund conditions | | `interruption` | Abandonment, minimum play, and restart conditions | | `resolution_source` | Official source, fallbacks, and missing-data handling | Filter rows with `comparison=different` to find known differences. Filter `unverified` rows to find evidence gaps. Values are extensible strings; display the description rather than guessing an unfamiliar value's meaning. A row can have a known difference and still contain an `unknown` source value. Agreement on observed clauses is not a complete contract review, and unknown values never mean equivalent or unrestricted. Rule evidence is attached to individual source contracts, not assumed from the provider's name. Market clauses are observed at ingestion. Kalshi product terms require the current series to link to the reviewed document; ProphetX requires its filings index to retain the reviewed NFL filing. Both require the document's SHA-256 to match the reviewed version; these checks are cached for at most five minutes. Changed or unavailable documents leave affected dimensions unknown. Known unresolved wording is not assigned an invented precedence. Read the evidence link for additional applicable terms. ## Example: a no-play difference [#example-a-no-play-difference] The September 2026 audit found Josh Allen passing-yard props with matching game/player/249.5 identities, but different non-participation treatment: | Source | Active player takes no snap | | ---------- | -------------------------------------------------------------------------------------------- | | Polymarket | Under wins; Over loses | | Kalshi | Settles at a pregame fair price, not necessarily zero or one | | ProphetX | Filed rules require active status and at least one snap for validity; no automatic Under win | A fair-price settlement, a 50-50 split, and a purchase-price refund are not interchangeable. A same-prop match is useful for discovery and comparison; it does not establish a risk-free arbitrage or a perfect hedge. For a comparison UI, show the prop's `display_name`, an overall `settlement_equivalence` badge, and the descriptions from each rule row. Keep unknown rows visible. A simplified excerpt (the actual response contains all nine rows and exact source market references) looks like: ```json { "display_name": "Josh Allen Passing yards O/U 249.5", "settlement_equivalence": "different", "rule_comparisons": [ { "rule": "non_participation", "label": "Player does not participate", "comparison": "different", "source_values": [ { "provider": "kalshi", "market_id": "KXNFLPASSYDS-26SEP13BUFHOU-BUFJALLEN17-250", "value": "active_no_snap_pregame_fair_price", "description": "If active but never takes a snap, settles at the pregame fair price. Inactive-player handling has not been verified here.", "evidence_url": "https://api.elections.kalshi.com/trade-api/v2/markets/KXNFLPASSYDS-26SEP13BUFHOU-BUFJALLEN17-250" }, { "provider": "polymarket", "market_id": "4080510", "value": "under_wins", "description": "If the player does not participate or is inactive, Under/No wins and Over/Yes loses.", "evidence_url": "https://gamma-api.polymarket.com/markets/4080510" } ] } ] } ``` ## Coverage and limitations [#coverage-and-limitations] | Provider | Player-prop admission in this release | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Kalshi | Thirteen NFL families listed below; validated NBA/WNBA full-game count props when listed | | Polymarket | Corresponding NFL families, including anytime/2+ touchdowns; validated NBA/WNBA full-game count props when listed | | Predict | No new admission: weekly NFL contracts are not single-game props; a bridge alone does not certify prop rules or selections | | ProphetX | Eleven NFL integer-stat families below, using the same primary strike as market detail; whole-number lines and contradictory selections are refused | | SX Bet, AlphaArcade, Hyperliquid | No verified game player-prop pairing in the audited NFL inventories | | NFL statistic | Kalshi | Polymarket | ProphetX | | -------------------------------- | ------ | ------------------ | --------------------------------------------------- | | Passing yards | Yes | Yes | Yes | | Rushing yards | Yes | Yes | Yes | | Receiving yards | Yes | Yes | Yes | | Receptions | Yes | Yes | Yes | | Passing completions | Yes | Yes | Yes | | Passing touchdowns | Yes | Yes | Excluded: filing assigns the scorer to the receiver | | Touchdowns scored (anytime / 2+) | Yes | Yes | No verified matching family | | Passing attempts | Yes | No observed family | Yes | | Passing interceptions | Yes | No observed family | Yes | | Rushing attempts | Yes | No observed family | Yes | | Rushing + receiving yards | Yes | No observed family | Yes | | Longest reception | Yes | No observed family | Yes | | Longest rush | Yes | No observed family | Yes | These are supported families, not a promise of a matching line at every venue. ProphetX alternate strikes are not exposed under the primary market ID: its market-detail response selects one current primary strike. Both selection labels and structured strike values must agree, including every order-book level's outcome identity. A whole-number primary line is excluded even when an alternate half-point line exists. The September 14 audit read all seven upstreams. Predict's 20 NFL statistical props were weekly passing-yard or anytime-touchdown contracts, not single-game contracts. SX Bet's 386 NFL markets, AlphaArcade's current NFL game children, and Hyperliquid's NFL outcomes provided no verified player-stat pairing. Provider tags alone are insufficient: AlphaArcade even carried an NFL tag on a SpaceX IPO question. Missing coverage stays explicit rather than creating a player match from unrelated or ambiguous data. The Chiefs' published roster names Kenneth Walker, while ESPN calls athlete `4567048` Kenneth Walker III. This reviewed alias is tied to that exact athlete and still requires a fresh matching-team roster. It does not remove suffixes from other players or resolve a name collision automatically. Availability is dynamic. NBA points inventory was empty on Kalshi during this audit; basketball evidence includes historical Polymarket contracts and the current published Kalshi terms, not a certified live NBA pair. This release does not claim a complete settlement profile for any audited provider pair; strict mode can legitimately return no player props. Previous NBA/WNBA name-and-line groupings are not grandfathered into strict equivalence. Canonical player keys use `espn__`. Same-prop keys identify the base prop; strict keys additionally identify a reviewed settlement group. One base prop can contain several equivalent subsets: strict mode emits each qualifying subset separately and never pulls in an incompatible third venue. Join native market/outcome IDs to [market detail](/api-reference/getMarket) for current data; this endpoint adds no prices, liquidity, or execution advice. ProphetX market detail converts its Trading API's American-odds offers into probabilities: the cheapest available back offer is the named selection's `ask`, and the opposite selection's ask supplies the complementary `bid`. `price` is the midpoint when both are present, not a last trade or a guaranteed fill. A one-sided book remains one-sided, and an empty book has null quotes. These quote conventions do not establish settlement equivalence or make an alternate strike interchangeable with the current primary strike. # Trading fees (/guides/trading-fees) `GET /v1/markets/{market_id}` returns a `trading_fees` object describing what the **venue** charges to trade that market. This is the prediction market's own fee, normalized across all seven platforms — it has nothing to do with your PredictorSDK plan. ## Why normalized price is not enough [#why-normalized-price-is-not-enough] Every venue's trading fee is non-linear in price, and none of them is a flat percentage. Two venues can quote the identical ask for the same canonical outcome and still differ by more than 100 bps in true cost — and which one is cheaper can invert during the trading day. A real example, read live on 2026-08-21. MLB full-game total, LAA @ TEX, line 8.5, Over. Both venues quoted `0.41 / 0.42`: | Market | Fee parameters | Fee on a $100 taker buy | Effective | | --------------------------------------- | -------------------------------------- | ----------------------- | --------- | | `kalshi:KXMLBTOTAL-26AUG212015LAATEX-9` | `quadratic`, rate `0.035` | $2.03 | 203 bps | | same market, after first pitch | `quadratic`, rate `0.07` | $4.06 | 406 bps | | `polymarket:3753849` | `quadratic`, rate `0.05`, exponent `1` | $2.90 | 290 bps | Kalshi is 87 bps cheaper before the game starts and 116 bps more expensive after. Both facts are readable in advance — the second one from `scheduled_change`. ## The model [#the-model] Every published fee across the seven platforms is a **per-share fee times the traded share count**. Only the price term differs, so one descriptor expresses all of them exactly: ``` fee = shares × f(price) then apply `rounding` ``` | `model` | `f(price)` | Used by | | ----------- | ----------------------------------------- | ------------------------------------- | | `quadratic` | `rate × (price × (1 − price)) ^ exponent` | Kalshi, Polymarket, AlphaArcade | | `min_price` | `rate × min(price, 1 − price)` | Predict | | `notional` | `rate × price` | Hyperliquid | | `none` | `0` | fee-free markets, and most maker legs | `rate` is a decimal fraction, never basis points: `0.07` means 7% of the price term. The number you actually compare across venues is the **effective rate on notional**, which is `f(price) / price`. For `quadratic` at `exponent: 1` that reduces to `rate × (1 − price)` — so a single configured basis-point rate is exact at one price and wrong everywhere else. At `rate: 0.07` the effective cost runs from 693 bps at a price of 0.01 down to 7 bps at 0.99. `min_price` is a **tent, not a parabola**: its effective rate is flat at `rate` for every price at or below 0.5 and only declines above it. Do not fold it into `quadratic`. ```ts function effectiveRate(leg: MarketDetailFeeLeg, price: number): number { switch (leg.model) { case "quadratic": return (leg.rate! * Math.pow(price * (1 - price), leg.exponent!)) / price; case "min_price": return (leg.rate! * Math.min(price, 1 - price)) / price; case "notional": return leg.rate!; case "none": return 0; } } ``` Both the `taker` and `maker` legs are always returned and you pick. PredictorSDK does not infer which side you will be — that needs an order it has not seen. ## Read `availability` before you read the rate [#read-availability-before-you-read-the-rate] `availability` says what is **known**; the legs say what is **charged**. They are deliberately on separate axes, so all four meaningful states are distinguishable: | `availability` | Meaning | Where you'll see it | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `published` | The full parameter set for this market is known. | Kalshi, Polymarket, Predict, AlphaArcade | | `partial` | Fees may be charged, but something upstream was unreadable. Nothing is guessed. | a Polymarket market with fees on and no readable schedule; a Kalshi market whose pending-change list could not be read | | `account_specific` | Rates exist but are set per trading account and need your own venue credentials. | SX Bet, Hyperliquid | | `unpublished` | The venue publishes no fee model for this market at all. | reserved | | `unavailable` | Our own bounded parameter fetch failed or timed out, or the venue exposes no per-market fee parameters at all. Identity and pricing are still served. | a degraded Kalshi lookup; every ProphetX market (the Market Maker API publishes no fee fields on the market record) | A **fee-free market is not a gap**: it is `availability: "published"` with `model: "none"` and `rate: 0` — an asserted zero. Polymarket's geopolitics and world-events markets are fee-free by policy, and so are 14 Kalshi series. For `account_specific` venues, read your own rates directly: SX Bet's authenticated `GET /user/fees-v3`, or Hyperliquid's `POST /info {"type":"userFees"}` (public for any address). A non-null `model` with a null `rate` — which is how Hyperliquid reports — means the shape is known and the magnitude is not. ## Two fields that change the arithmetic [#two-fields-that-change-the-arithmetic] **`charge_basis`** tells you what the fee is assessed on. `fill` means it is taken when the order fills, from the traded shares and the fill price. `settlement_profit` means it is taken at settlement, on profit, and only on a position that **won** — SX Bet works this way. That is an expected-value haircut, not an entry cost; modelling it as a percentage of notional is wrong about the shape before it is wrong about the rate. **`scheduled_change`** carries the next published parameter change and its effective time. It is omitted when there is none. Kalshi publishes these today, and it matters: the entire MLB family currently runs at a half-rate `fee_multiplier` with a per-event override restoring the full rate at first pitch, so the fee doubles on a game that is already trading. Once that change fires, it stops being a `scheduled_change` and starts being the rate in `taker`/`maker` — the same market reports `rate: 0.035` with a pending change before first pitch, and `rate: 0.07` with no pending change after. If `availability` is `partial`, an omitted `scheduled_change` means the pending list was unreadable, not that no change is coming. ```json "scheduled_change": { "effective_at": "2026-08-22T00:15:00Z", "taker": { "model": "quadratic", "rate": 0.07, "exponent": 1, "rounding": { "direction": "up", "increment": 0.0001 } }, "maker": { "model": "none", "rate": 0, "exponent": null, "rounding": null } } ``` ## Rounding, and where the number will still be slightly off [#rounding-and-where-the-number-will-still-be-slightly-off] `rounding` is the venue's published rule — `{ "direction": "up" | "nearest", "increment": }` — or `null` where the venue publishes none (Predict). Treat `null` as unknown rather than as "no rounding", which would understate small trades. Kalshi additionally charges a per-fill rounding fee that restores your balance precision, offset by a rebate once accumulated rounding exceeds $0.01. That depends on how your order fragments across fills and on your member type, so it is not predictable pre-trade and is not modelled here. Expect a small positive difference between the fee you compute and the fee Kalshi charges across many partial fills. ## What is deliberately not here [#what-is-deliberately-not-here] These are all real and they all move the number, which is why they are documented rather than silently omitted — but they are not per-market venue parameters, so they are not fields: * **Rebates** — Polymarket's maker-rebate and taker-rebate programs. * **Per-account discounts** — Predict's 10% invite discount. `rate` is the published base; a referred account pays less. * **Volume and staking tiers** — Hyperliquid's fee tier depends on rolling 14-day volume and staked HYPE. * **Third-party pass-through** — Kalshi fees charged by an FCM, and Polymarket builder fees, which depend on the front end you route through. * **Maker/taker inference.** Both legs are returned; deciding which applies is execution policy. ## Cost of the lookup [#cost-of-the-lookup] Polymarket, Predict and AlphaArcade carry their fee parameters on the same market record the lookup already fetches, so `trading_fees` costs them nothing. SX Bet, Hyperliquid and ProphetX need no fetch at all. Only Kalshi costs extra upstream hops — its parameters live on the parent series plus any scheduled per-event override — and those are bounded and cached. `source` tells you which case you are in: `market_record`, `series_record`, or `venue_schedule`. `observed_at` is when PredictorSDK read the parameters. It is an observation timestamp, not an upstream one — which is why it is not called `as_of`, the way `pricing.as_of` is. It is `null` for `venue_schedule` (nothing is read at request time) and for `unavailable`. `pricing.observed_at` carries the same meaning for the quotes, and `pricing.as_of_kind` says whether `pricing.as_of` is a quote time you can bound in seconds or a record stamp you cannot. # Overview (/) PredictorSDK is an API that matches sports prediction markets across Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade (moneylines), and ProphetX (moneylines and verified NFL props), and aggregates market/event detail from Hyperliquid, AlphaArcade, and ProphetX too. It normalizes equivalent markets into canonical events where cross-platform matching is available, with a unified catalog for provider-native markets. ## Public API scope [#public-api-scope] These docs and the OpenAPI reference cover the public consumer API only. Internal dashboard billing routes and provider webhook handlers are intentionally excluded from the public contract. ## What it does [#what-it-does] * **Cross-platform market matching** — merge equivalent markets across sources into one event with stable IDs and normalized sub-market keys * **Unified market catalog** — list and inspect provider-native markets across supported platforms, including Hyperliquid HIP-4 outcome markets and AlphaArcade binary/multi-choice markets * **OpenAPI-first, SDK-generated** — the public API surface is spec-driven, so SDKs regenerate cleanly with no manual override layer. [Download the canonical OpenAPI YAML](/openapi.yaml). * **Direct canonical and source lookup** — look up matching markets by canonical event ID, Kalshi event ticker, Polymarket slug, Predict market ID, or SX.Bet market ID ## How it works [#how-it-works] 1. **Ingest** — fetch active market catalogs from connected platforms and normalize source-specific fields 2. **Match** — group equivalent markets into game-level events and cross-source sub-markets 3. **Serve** — query one API endpoint or generated SDK method to consume unified market data ## Coverage [#coverage] ### Leagues [#leagues] Canonical event IDs are `{league}-{away}-{home}-{date}`, so the league is the first segment — `nba-okc-sas-2026-10-20` is an NBA game. | League | Platforms matched | | ------ | ----------------------------------------------------------------------------------------------------- | | NBA | Kalshi, Polymarket, Predict, SX.Bet, ProphetX (moneyline) | | WNBA | Kalshi, Polymarket, SX.Bet, AlphaArcade (moneyline) | | NHL | Kalshi, Polymarket, Predict, SX.Bet, ProphetX (moneyline) | | MLB | Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade (moneyline), ProphetX (moneyline) | | NFL | Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade (moneyline), ProphetX (moneyline and verified props) | Predict does not currently list WNBA games, and lists NFL only in season. NFL is full-game moneyline, spread, and total, plus verified player props with [explicit rule comparisons](/guides/player-prop-matching). ProphetX lists only the current week's fixtures per league, so it carries a game once the venue opens it (NFL and MLB verified live; NBA and NHL are mapped but were out of season when the provider shipped). ### Market types [#market-types] | Market Type | Platforms | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Moneyline (full/1H) | Kalshi, Polymarket, Predict, SX.Bet, AlphaArcade (full game only), ProphetX (full game only) | | Spread (full/1H) | Kalshi, Polymarket, SX.Bet | | Total (full/1H) | Kalshi, Polymarket, SX.Bet | | NFL player props (same-prop policy) | Kalshi, Polymarket, ProphetX; availability varies by statistic and line | | Player Points | Kalshi, Polymarket | | Player Rebounds | Kalshi, Polymarket | | Player Assists | Kalshi, Polymarket | | HIP-4 outcome markets | Hyperliquid | | Binary/multi-choice markets | AlphaArcade | | Sports exchange game lines, halves, and player props | ProphetX | All player-prop rows require verified identity and the selected settlement policy. See the [player-prop guide](/guides/player-prop-matching) for current coverage. ## Next steps [#next-steps] * [Quickstart](/quickstart) — make your first API call in under a minute * [Authentication](/authentication) — get and use your API key * [API Reference](/api-reference) — full endpoint documentation with interactive playground # Quickstart (/quickstart) ## Make your first call [#make-your-first-call] Start here — this call takes no identifiers, so there is nothing to look up first. It returns a page of currently matched sports events keyed by canonical event ID. ```bash curl -s \ -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports?limit=2" ``` ```ts import { PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); const response = await client.getSportsMatchingMarkets({ limit: 2 }); console.log(response.markets); ``` ```python from predictorsdk import PredictorSDK client = PredictorSDK(token="YOUR_API_KEY") response = client.get_sports_matching_markets(limit=2) print(response.markets) ``` ```go import ( "context" "fmt" predictorsdk "github.com/PredictorSDK/sdk-go" predictorclient "github.com/PredictorSDK/sdk-go/client" "github.com/PredictorSDK/sdk-go/option" ) client := predictorclient.New(option.WithToken("YOUR_API_KEY")) response, err := client.GetSportsMatchingMarkets(context.TODO(), &predictorsdk.GetSportsMatchingMarketsRequest{ Limit: predictorsdk.Int(2), }) if err != nil { panic(err) } fmt.Println(response.Markets) ``` Each key is a canonical event ID; each value lists that event on every platform carrying it, with that platform's own identifiers: ```json { "markets": { "nba-okc-sas-2026-10-20": [ { "platform": "KALSHI", "event_id": "KXNBAGAME-26OCT20OKCSAS", "event_ticker": "KXNBAGAME-26OCT20OKCSAS", "market_tickers": [ "KXNBAGAME-26OCT20OKCSAS-OKC", "KXNBAGAME-26OCT20OKCSAS-SAS" ] }, { "platform": "SXBET", "event_id": "L19766755", "market_id": "0xb4d047a709aae881e5ccad9d123592967644ee1df1f17078c762b388e41b81c5" } ] }, "pagination": { "limit": 2, "total": 106, "has_more": true, "next_cursor": "eyJ..." } } ``` Game tickers, event slugs, and canonical event keys are delisted once an event settles, so the specific values shown throughout these docs will stop resolving. Take live ones from the call above — that is the whole reason this page starts with a call that needs no identifier. ## Look up one of those events [#look-up-one-of-those-events] Take any key or platform identifier from the response above and look it up directly, instead of walking list pages to find it again: ```bash curl -s \ -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports?kalshi_event_ticker=KXNBAGAME-26OCT20OKCSAS" ``` ```ts const response = await client.getSportsMatchingMarkets({ kalshiEventTicker: "KXNBAGAME-26OCT20OKCSAS", }); console.log(response.markets); ``` ```python response = client.get_sports_matching_markets( kalshi_event_ticker="KXNBAGAME-26OCT20OKCSAS" ) print(response.markets) ``` ```go response, err := client.GetSportsMatchingMarkets(context.TODO(), &predictorsdk.GetSportsMatchingMarketsRequest{ KalshiEventTicker: []*string{predictorsdk.String("KXNBAGAME-26OCT20OKCSAS")}, }) if err != nil { panic(err) } fmt.Println(response.Markets) ``` A lookup is keyed by whatever you asked for, and returns the full match with no pagination block: ```json { "markets": { "KXNBAGAME-26OCT20OKCSAS": [ { "platform": "KALSHI", "event_id": "KXNBAGAME-26OCT20OKCSAS", "event_ticker": "KXNBAGAME-26OCT20OKCSAS", "market_tickers": [ "KXNBAGAME-26OCT20OKCSAS-OKC", "KXNBAGAME-26OCT20OKCSAS-SAS" ] }, { "platform": "SXBET", "event_id": "L19766755", "market_id": "0xb4d047a709aae881e5ccad9d123592967644ee1df1f17078c762b388e41b81c5", "outcome_ids": [ "outcomeOne", "outcomeTwo" ] } ] } } ``` Coverage is NBA, WNBA, NHL, MLB, and NFL. The league is the first segment of the canonical event ID. ## Add exact submarket identity [#add-exact-submarket-identity] Pass `include_submarkets=true` to get a `canonical_events` map alongside `markets`, with normalized market type, segment, signed line, outcomes, and each source's exact native market and outcome references: ```bash curl -s \ -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.predictorsdk.com/v1/matching-markets/sports?event_id=nba-okc-sas-2026-10-20&include_submarkets=true" ``` `canonical_events` carries identity and exact native references only — prices and executable depth remain on market and venue surfaces. Join its `source_markets[].market_id` values to [`GET /v1/markets/{market_id}`](/api-reference/getMarket) for quotes, and its `outcomes[].outcome_id` values to that response's `outcomes[].outcome_id` to line up the selections. Every provider joins on both. Polymarket, Predict and AlphaArcade publish a globally unique per-outcome token; SX Bet and Kalshi have none, so their references are market-scoped and read together with `market_id` — `outcomeOne` / `outcomeTwo` on SX Bet, `yes` / `no` on Kalshi. One provider can appear more than once under the same submarket, because a venue may model one canonical market as several native ones. Kalshi lists a game moneyline as one binary market per team, so a Kalshi moneyline shows up twice — once per team ticker, each with its own `yes` / `no` outcomes. Group by `provider` if you want one row per venue. ## Next steps [#next-steps] * [Authentication](/authentication) -- add your API key * [Filtering](/guides/filtering) -- look up canonical event IDs or provider identifiers * [Plans & Limits](/guides/plans) -- read the public plan catalog with `GET /v1/plans` * [Pagination](/guides/pagination) -- walk large result sets with cursors * [API Reference](/api-reference) -- explore all endpoints # Go SDK Reference (/sdk/go) The Go client is auto-generated from the OpenAPI spec and published as [`github.com/PredictorSDK/sdk-go`](https://github.com/PredictorSDK/sdk-go). ## Client surfaces [#client-surfaces] | Surface | Endpoint | | ---------------------------------------------------- | ------------------------------------- | | `client.GetPlans(ctx)` | `GET /v1/plans` (no auth required) | | `client.GetSportsMatchingMarkets(ctx, request)` | `GET /v1/matching-markets/sports` | | `client.GetMarkets(ctx, request)` | `GET /v1/markets` | | `client.GetCategories(ctx)` | `GET /v1/categories` | | `client.GetMarket(ctx, request)` | `GET /v1/markets/{market_id}` | | `client.GetEvent(ctx, request)` | `GET /v1/events/{event_id}` | | `client.GetBinanceCryptoPrices(ctx, request)` | `GET /v1/crypto-prices/binance` | | `client.GetPolymarketWallet(ctx, request)` | `GET /v1/polymarket/wallet` | | `client.ListPolymarketWalletPositions(ctx, request)` | `GET /v1/polymarket/wallet/positions` | ## Usage [#usage] ```go import ( "context" "fmt" predictorclient "github.com/PredictorSDK/sdk-go/client" "github.com/PredictorSDK/sdk-go/option" ) func main() { client := predictorclient.New(option.WithToken("YOUR_API_KEY")) response, err := client.GetSportsMatchingMarkets(context.TODO(), nil) if err != nil { panic(err) } fmt.Println(response.Markets) categories, _ := client.GetCategories(context.TODO()) plans, _ := client.GetPlans(context.TODO()) // Public; no API key is sent. } ``` ## Configuration [#configuration] Options are passed to `predictorclient.New()`: | Option | Description | | ------------------------------- | --------------------------- | | `option.WithToken(token)` | Your API key (Bearer token) | | `option.WithBaseURL(url)` | Override the base URL | | `option.WithHTTPClient(client)` | Custom `*http.Client` | | `option.WithHTTPHeader(header)` | Additional HTTP headers | ## Key types [#key-types] All types live in the root `predictorsdk` package: ```go import predictorsdk "github.com/PredictorSDK/sdk-go" ``` ### SportsMatchingResponse [#sportsmatchingresponse] | Field | Type | Description | | ----------------- | ---------------------------------- | --------------------------------------------------------------------------------- | | `Markets` | `map[string][]*PlatformMarket` | Map of identifier to platform market arrays | | `CanonicalEvents` | `map[string]*CanonicalSportsEvent` | Opt-in exact event/submarket/source identity map when `IncludeSubmarkets` is true | | `Pagination` | `*PaginationBlock` | Present in list mode; `nil` when a platform-ID filter is used | ### PaginationBlock [#paginationblock] | Field | Type | Description | | ------------ | --------- | ------------------------------------------------------ | | `Limit` | `int` | Echoes the `limit` request param (default 25, max 100) | | `Total` | `int` | Total matching items across all pages | | `HasMore` | `bool` | `true` when more pages exist | | `NextCursor` | `*string` | Pass back via `Cursor` for the next page | ### PlatformMarket [#platformmarket] | Field | Type | Description | | --------------- | ------------------------ | ------------------------------------------------------------------------- | | `Platform` | `PlatformMarketPlatform` | `KALSHI`, `POLYMARKET`, `PREDICT`, `SXBET`, `ALPHA-ARCADE`, or `PROPHETX` | | `EventTicker` | `*string` | Kalshi event ticker (present when platform is `KALSHI`) | | `MarketTickers` | `[]string` | Kalshi market tickers (present when platform is `KALSHI`) | | `MarketSlug` | `*string` | Polymarket market slug (present when platform is `POLYMARKET`) | | `TokenIDs` | `[]string` | Polymarket token IDs (present when platform is `POLYMARKET`) | | `MarketID` | `*string` | Source market ID (present for other platforms) | | `OutcomeIDs` | `[]string` | Source outcome IDs (present for other platforms) | ### GetSportsMatchingMarketsRequest [#getsportsmatchingmarketsrequest] Pass `nil` to fetch all markets with defaults, or provide filter fields. Only one platform-ID filter type may be used per request. | Field | Type | Description | | ---------------------- | ----------- | ------------------------------------------------------------------------------------ | | `Limit` | `*int` | Results per page (1–100, default 25). Ignored in lookup mode. | | `Cursor` | `*string` | Cursor from a previous `Pagination.NextCursor` | | `IncludeSettled` | `*bool` | Also return events whose game date has passed (default `false`) | | `IncludeSubmarkets` | `*bool` | Add canonical event, line, outcome, and exact provider ID mappings (default `false`) | | `EventID` | `[]*string` | Canonical event key(s) for exact lookup (max 100 unique values) | | `KalshiEventTicker` | `[]*string` | Kalshi event ticker(s) (max 100 unique values) | | `PolymarketMarketSlug` | `[]*string` | Polymarket market slug(s) (max 100 unique values) | | `PredictMarketID` | `[]*string` | Predict market ID(s) (max 100 unique values) | | `SxbetMarketID` | `[]*string` | SX Bet market ID(s) (max 100 unique values) | | `AlphaArcadeMarketID` | `[]*string` | AlphaArcade market ULID(s) (max 100 unique values) | | `ProphetxEventID` | `[]*string` | ProphetX event ID(s) (max 100 unique values) | | `ProphetxMarketID` | `[]*string` | ProphetX market ID(s) `:` (max 100 unique values) | | `PlayerPropMatch` | `*string` | Player-prop policy (`strict` or `same_prop`); requires `IncludeSubmarkets` | ```go // Paginated list — walk every page. var cursor *string for { page, err := client.GetSportsMatchingMarkets(ctx, &predictorsdk.GetSportsMatchingMarketsRequest{ Limit: predictorsdk.Int(50), Cursor: cursor, }) if err != nil { return err } // ... process page.Markets if page.Pagination == nil || !page.Pagination.HasMore { break } cursor = page.Pagination.NextCursor } ``` See the [Pagination guide](/guides/pagination) for the full walkthrough. ### MarketsListResponse [#marketslistresponse] The response from `GetMarkets`. Contains `Data`, catalog `Snapshot` freshness, and `Pagination`. | Field | Type | Description | | ------------ | ------------------ | ------------------------------------------------------------------------------------------------------ | | `Data` | `[]*UnifiedMarket` | Array of markets for the current page | | `Snapshot` | `*MarketsSnapshot` | Catalog freshness; `ObservedAt` is when the oldest provider snapshot in the bound page scope was built | | `Pagination` | `*PaginationBlock` | Pagination metadata | ### UnifiedMarket [#unifiedmarket] A market from any supported prediction market provider. | Field | Type | Description | | ---------- | ----------------------- | ---------------------------------------------------------------------------------------- | | `ID` | `string` | Composite ID in the format `{provider}:{provider_id}` | | `Provider` | `UnifiedMarketProvider` | `kalshi`, `polymarket`, `predict`, `sxbet`, `hyperliquid`, `alpha-arcade`, or `prophetx` | | `Title` | `string` | Human-readable market title/question | | `Category` | `MarketCategory` | Canonical top-level category, such as `sports`, `politics`, or `crypto` | ### GetMarketsRequest [#getmarketsrequest] Pass `nil` to fetch all markets with defaults, or provide pagination fields. | Field | Type | Description | | ---------- | ---------------------------- | ---------------------------------------------------------------------- | | `Limit` | `*int` | Results per page (1–100, default 25) | | `Cursor` | `*string` | Cursor from a previous `Pagination.NextCursor` | | `Category` | `*MarketCategory` | Optional canonical category filter | | `Provider` | `*GetMarketsRequestProvider` | Optional canonical provider filter; replay cursors with the same value | ```go // Paginated list — walk every page. var cursor *string for { page, err := client.GetMarkets(ctx, &predictorsdk.GetMarketsRequest{ Limit: predictorsdk.Int(50), Cursor: cursor, }) if err != nil { return err } // ... process page.Data if page.Pagination == nil || !page.Pagination.HasMore { break } cursor = page.Pagination.NextCursor } ``` ### Pointer helpers [#pointer-helpers] The `predictorsdk` package provides pointer helpers for building requests: ```go import predictorsdk "github.com/PredictorSDK/sdk-go" request := &predictorsdk.GetSportsMatchingMarketsRequest{ KalshiEventTicker: []*string{predictorsdk.String("KXNBAGAME-26OCT20OKCSAS")}, } ``` Available helpers: `String`, `Bool`, `Int`, `Float64`, `Time`, `UUID`. ## Raw responses [#raw-responses] Use `WithRawResponse` to access HTTP metadata: ```go import ( "context" "fmt" predictorclient "github.com/PredictorSDK/sdk-go/client" "github.com/PredictorSDK/sdk-go/option" ) func main() { client := predictorclient.New(option.WithToken("YOUR_API_KEY")) raw, err := client.WithRawResponse.GetSportsMatchingMarkets(context.TODO(), nil) if err != nil { panic(err) } fmt.Println(raw.StatusCode) fmt.Println(raw.Body.Markets) } ``` ## Error handling [#error-handling] The SDK returns typed errors for known HTTP error codes: | Error type | Status code | Description | | -------------------------- | ----------- | --------------------------------------------------------------------------------------- | | `*BadRequestError` | 400 | Invalid query parameters | | `*UnauthorizedError` | 401 | Missing or invalid API key | | `*PaymentRequiredError` | 402 | Monthly allowance exhausted or subscription payment recovery required | | `*ForbiddenError` | 403 | Insufficient permissions | | `*NotFoundError` | 404 | Market or event not found | | `*ConflictError` | 409 | Bare identifier resolves on multiple providers; retry with `platform` or a composite ID | | `*TooManyRequestsError` | 429 | Rate limit exceeded | | `*BadGatewayError` | 502 | Upstream provider or API-key authorization dependency failure | | `*ServiceUnavailableError` | 503 | Service temporarily unavailable | Retries prefer `Retry-After` on `429`, then the second-based `X-RateLimit-Reset`. The runtime also accepts legacy millisecond reset values and will not misinterpret them as a 60-second wait. ```go import ( "context" "errors" "fmt" predictorclient "github.com/PredictorSDK/sdk-go/client" predictorsdk "github.com/PredictorSDK/sdk-go" "github.com/PredictorSDK/sdk-go/option" ) func main() { client := predictorclient.New(option.WithToken("YOUR_API_KEY")) _, err := client.GetSportsMatchingMarkets(context.TODO(), &predictorsdk.GetSportsMatchingMarketsRequest{ KalshiEventTicker: []*string{predictorsdk.String("KXWNBAGAME-26AUG28TORLV")}, PolymarketMarketSlug: []*string{predictorsdk.String("wnba-tor-las-2026-08-28")}, }) var badReq *predictorsdk.BadRequestError if errors.As(err, &badReq) { fmt.Println("Bad request:", badReq.Body.Error) } } ``` # Installation (/sdk/installation) PredictorSDK clients are auto-generated from the OpenAPI spec using [Fern](https://buildwithfern.com). All SDKs cover the same endpoint surface with language-native naming and types. ## Install [#install] ```bash npm install @predictorsdk/client ``` ```bash pip install predictorsdk ``` ```bash go get github.com/PredictorSDK/sdk-go ``` ## Initialize the client [#initialize-the-client] ```ts import { PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); ``` ```python from predictorsdk import PredictorSDK client = PredictorSDK(token="YOUR_API_KEY") ``` ```go import predictorclient "github.com/PredictorSDK/sdk-go/client" import "github.com/PredictorSDK/sdk-go/option" client := predictorclient.New(option.WithToken("YOUR_API_KEY")) ``` ## Configuration options [#configuration-options] | Option | Type | Description | | ------- | -------- | --------------------------- | | `token` | `string` | Your API key (Bearer token) | ## Available SDKs [#available-sdks] | Language | Package | Status | | ---------- | ---------------------------------------------------------------------------- | --------- | | TypeScript | [`@predictorsdk/client`](https://www.npmjs.com/package/@predictorsdk/client) | Published | | Python | [`predictorsdk`](https://pypi.org/project/predictorsdk/) | Published | | Go | [`github.com/PredictorSDK/sdk-go`](https://github.com/PredictorSDK/sdk-go) | Published | All SDKs are auto-generated from the same OpenAPI spec to ensure consistency. # Python SDK Reference (/sdk/python) The Python client is auto-generated from the OpenAPI spec and published as [`predictorsdk`](https://pypi.org/project/predictorsdk/) on PyPI. ## Client surfaces [#client-surfaces] | Surface | Endpoint | | ---------------------------------------------- | ------------------------------------- | | `client.get_plans()` | `GET /v1/plans` (no auth required) | | `client.get_sports_matching_markets(...)` | `GET /v1/matching-markets/sports` | | `client.get_markets(...)` | `GET /v1/markets` | | `client.get_categories()` | `GET /v1/categories` | | `client.get_market(market_id, ...)` | `GET /v1/markets/{market_id}` | | `client.get_event(event_id, ...)` | `GET /v1/events/{event_id}` | | `client.get_binance_crypto_prices(...)` | `GET /v1/crypto-prices/binance` | | `client.get_polymarket_wallet(...)` | `GET /v1/polymarket/wallet` | | `client.list_polymarket_wallet_positions(...)` | `GET /v1/polymarket/wallet/positions` | The SDK provides both synchronous (`PredictorSDK`) and asynchronous (`AsyncPredictorSDK`) clients. ## Sync client [#sync-client] ```python from predictorsdk import PredictorSDK client = PredictorSDK(token="YOUR_API_KEY") response = client.get_sports_matching_markets() print(response.markets) categories = client.get_categories() plans = client.get_plans() # Public; no API key is sent. ``` ## Async client [#async-client] ```python import asyncio from predictorsdk import AsyncPredictorSDK client = AsyncPredictorSDK(token="YOUR_API_KEY") async def main(): response = await client.get_sports_matching_markets() print(response.markets) asyncio.run(main()) ``` ## Configuration [#configuration] | Parameter | Type | Description | | -------------- | ------------------------- | ------------------------------------------ | | `token` | `str` | Your API key (Bearer token) | | `base_url` | `str \| None` | Override the base URL | | `environment` | `PredictorSDKEnvironment` | API environment (defaults to `PRODUCTION`) | | `timeout` | `float \| None` | Request timeout in seconds | | `httpx_client` | `httpx.Client \| None` | Custom httpx client instance | ## Key types [#key-types] ### SportsMatchingResponse [#sportsmatchingresponse] The response from `get_sports_matching_markets`. Contains a `markets` map keyed by identifier, plus a `pagination` block in list mode. | Field | Type | Description | | ------------------ | ----------------------------------------- | ---------------------------------------------------------------------------------- | | `markets` | `Dict[str, List[PlatformMarket]]` | Map of identifier to platform market arrays | | `canonical_events` | `Dict[str, CanonicalSportsEvent] \| None` | Opt-in exact event/submarket/source identity map when `include_submarkets` is true | | `pagination` | `PaginationBlock \| None` | Present in list mode; omitted when a platform-ID filter is used | ### PaginationBlock [#paginationblock] | Field | Type | Description | | ------------- | ------------- | ------------------------------------------------------ | | `limit` | `int` | Echoes the `limit` request param (default 25, max 100) | | `total` | `int` | Total matching items across all pages | | `has_more` | `bool` | `True` when more pages exist | | `next_cursor` | `str \| None` | Pass back via `cursor` for the next page | ### PlatformMarket [#platformmarket] A market listing on a specific platform. | Field | Type | Description | | ---------------- | ------------------------ | ------------------------------------------------------------------------- | | `platform` | `PlatformMarketPlatform` | `KALSHI`, `POLYMARKET`, `PREDICT`, `SXBET`, `ALPHA-ARCADE`, or `PROPHETX` | | `event_ticker` | `str \| None` | Kalshi event ticker (present when platform is `KALSHI`) | | `market_tickers` | `List[str] \| None` | Kalshi market tickers (present when platform is `KALSHI`) | | `market_slug` | `str \| None` | Polymarket market slug (present when platform is `POLYMARKET`) | | `token_ids` | `List[str] \| None` | Polymarket token IDs (present when platform is `POLYMARKET`) | | `market_id` | `str \| None` | Source market ID (present for other platforms) | | `outcome_ids` | `List[str] \| None` | Source outcome IDs (present for other platforms) | ### Method parameters [#method-parameters] `get_sports_matching_markets` accepts the following keyword arguments. Only one platform-ID filter type may be used per request. | Parameter | Type | Description | | ------------------------ | ------------------------------ | ------------------------------------------------------------------------------------ | | `limit` | `int \| None` | Results per page (1–100, default 25). Ignored in lookup mode. | | `cursor` | `str \| None` | Cursor from a previous `pagination.next_cursor` | | `include_settled` | `bool \| None` | Also return events whose game date has passed (default `False`) | | `include_submarkets` | `bool \| None` | Add canonical event, line, outcome, and exact provider ID mappings (default `False`) | | `event_id` | `str \| Sequence[str] \| None` | Canonical event key(s) for exact lookup (max 100 unique values) | | `kalshi_event_ticker` | `str \| Sequence[str] \| None` | Kalshi event ticker(s) (max 100 unique values) | | `polymarket_market_slug` | `str \| Sequence[str] \| None` | Polymarket market slug(s) (max 100 unique values) | | `predict_market_id` | `str \| Sequence[str] \| None` | Predict market ID(s) (max 100 unique values) | | `sxbet_market_id` | `str \| Sequence[str] \| None` | SX Bet market ID(s) (max 100 unique values) | | `alpha_arcade_market_id` | `str \| Sequence[str] \| None` | AlphaArcade market ULID(s) (max 100 unique values) | | `prophetx_event_id` | `str \| Sequence[str] \| None` | ProphetX event ID(s) (max 100 unique values) | | `prophetx_market_id` | `str \| Sequence[str] \| None` | ProphetX market ID(s) `:` (max 100 unique values) | | `player_prop_match` | `str \| None` | Player-prop policy (`strict` or `same_prop`); requires `include_submarkets=True` | ```python # Paginated list — walk every page. cursor = None while True: page = client.get_sports_matching_markets(limit=50, cursor=cursor) # ... process page.markets if not page.pagination or not page.pagination.has_more: break cursor = page.pagination.next_cursor ``` See the [Pagination guide](/guides/pagination) for the full walkthrough. ### MarketsListResponse [#marketslistresponse] The response from `get_markets`. Contains `data`, catalog `snapshot` freshness, and `pagination`. | Field | Type | Description | | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------- | | `data` | `List[UnifiedMarket]` | Array of markets for the current page | | `snapshot` | `MarketsSnapshot` | Catalog freshness; `observed_at` is when the oldest provider snapshot in the bound page scope was built | | `pagination` | `PaginationBlock` | Pagination metadata | ### UnifiedMarket [#unifiedmarket] A market from any supported prediction market provider. | Field | Type | Description | | ---------- | ----------------------- | ---------------------------------------------------------------------------------------- | | `id` | `str` | Composite ID in the format `{provider}:{provider_id}` | | `provider` | `UnifiedMarketProvider` | `kalshi`, `polymarket`, `predict`, `sxbet`, `hyperliquid`, `alpha-arcade`, or `prophetx` | | `title` | `str` | Human-readable market title/question | | `category` | `MarketCategory` | Canonical top-level category, such as `sports`, `politics`, or `crypto` | ### get\_markets parameters [#get_markets-parameters] | Parameter | Type | Description | | ---------- | ----------------------------------- | ---------------------------------------------------------------------- | | `limit` | `int \| None` | Results per page (1–100, default 25) | | `cursor` | `str \| None` | Cursor from a previous `pagination.next_cursor` | | `category` | `MarketCategory \| None` | Optional canonical category filter | | `provider` | `GetMarketsRequestProvider \| None` | Optional canonical provider filter; replay cursors with the same value | ```python # Paginated list — walk every page. cursor = None while True: page = client.get_markets(limit=50, cursor=cursor) # ... process page.data if not page.pagination or not page.pagination.has_more: break cursor = page.pagination.next_cursor ``` ## Raw responses [#raw-responses] Use `with_raw_response` to access HTTP metadata alongside the parsed body: ```python raw = client.with_raw_response.get_sports_matching_markets() print(raw.status_code) print(raw.headers) print(raw.data.markets) ``` ## Error handling [#error-handling] The SDK raises typed exceptions for known HTTP error codes: | Error class | Status code | Description | | ------------------------- | ----------- | --------------------------------------------------------------------------------------- | | `BadRequestError` | 400 | Invalid query parameters | | `UnauthorizedError` | 401 | Missing or invalid API key | | `PaymentRequiredError` | 402 | Monthly allowance exhausted or subscription payment recovery required | | `ForbiddenError` | 403 | Insufficient permissions | | `NotFoundError` | 404 | Market or event not found | | `ConflictError` | 409 | Bare identifier resolves on multiple providers; retry with `platform` or a composite ID | | `TooManyRequestsError` | 429 | Rate limit exceeded | | `BadGatewayError` | 502 | Upstream provider or API-key authorization dependency failure | | `ServiceUnavailableError` | 503 | Service temporarily unavailable | Retries prefer `Retry-After` on `429`, then the second-based `X-RateLimit-Reset`. The runtime also accepts legacy millisecond reset values and will not misinterpret them as a 60-second wait. ```python from predictorsdk import PredictorSDK, BadRequestError client = PredictorSDK(token="YOUR_API_KEY") try: client.get_sports_matching_markets( kalshi_event_ticker="KXWNBAGAME-26AUG28TORLV", polymarket_market_slug="wnba-tor-las-2026-08-28", ) except BadRequestError as e: print(f"Bad request: {e.body.error}") ``` # TypeScript SDK Reference (/sdk/typescript) The TypeScript client is auto-generated from the OpenAPI spec and published as [`@predictorsdk/client`](https://www.npmjs.com/package/@predictorsdk/client) on npm. ## Client surfaces [#client-surfaces] | Surface | Endpoint | | ----------------------------------------------- | ------------------------------------- | | `client.getPlans()` | `GET /v1/plans` (no auth required) | | `client.getSportsMatchingMarkets(request)` | `GET /v1/matching-markets/sports` | | `client.getMarkets(request)` | `GET /v1/markets` | | `client.getCategories()` | `GET /v1/categories` | | `client.getMarket(request)` | `GET /v1/markets/{market_id}` | | `client.getEvent(request)` | `GET /v1/events/{event_id}` | | `client.getBinanceCryptoPrices(request)` | `GET /v1/crypto-prices/binance` | | `client.getPolymarketWallet(request)` | `GET /v1/polymarket/wallet` | | `client.listPolymarketWalletPositions(request)` | `GET /v1/polymarket/wallet/positions` | When you `await` an SDK call, it resolves to the parsed response body. Use `.withRawResponse()` if you also need headers or status metadata. ## Usage [#usage] ```ts import { PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); const response = await client.getSportsMatchingMarkets(); console.log(response.markets); const categories = await client.getCategories(); const plans = await client.getPlans(); // Public; no API key is sent. ``` ## Configuration [#configuration] | Option | Type | Description | | ------------------ | ------------------------- | ------------------------------------------ | | `token` | `string` | Your API key (Bearer token) | | `baseUrl` | `string` | Override the base URL | | `environment` | `PredictorSDKEnvironment` | API environment (defaults to `Production`) | | `timeoutInSeconds` | `number` | Request timeout in seconds (default: 60) | | `maxRetries` | `number` | Number of retries on failure (default: 2) | | `fetch` | `typeof fetch` | Custom fetch implementation | ## Key types [#key-types] ### SportsMatchingResponse [#sportsmatchingresponse] The response from `getSportsMatchingMarkets`. Contains a `markets` map keyed by identifier, plus a `pagination` block in list mode. | Field | Type | Description | | ----------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- | | `markets` | `Record` | Map of identifier to platform market arrays | | `canonicalEvents` | `Record \| undefined` | Opt-in exact event/submarket/source identity map when `includeSubmarkets` is true | | `pagination` | `PaginationBlock \| undefined` | Present in list mode; omitted when a platform-ID filter is used | ### PaginationBlock [#paginationblock] | Field | Type | Description | | ------------ | --------------------- | ------------------------------------------------------ | | `limit` | `number` | Echoes the `limit` request param (default 25, max 100) | | `total` | `number` | Total matching items across all pages | | `hasMore` | `boolean` | `true` when more pages exist | | `nextCursor` | `string \| undefined` | Pass back via `cursor` for the next page | ### PlatformMarket [#platformmarket] A market listing on a specific platform. | Field | Type | Description | | --------------- | ------------------------ | ------------------------------------------------------------------------- | | `platform` | `PlatformMarketPlatform` | `KALSHI`, `POLYMARKET`, `PREDICT`, `SXBET`, `ALPHA-ARCADE`, or `PROPHETX` | | `eventTicker` | `string \| undefined` | Kalshi event ticker (present when platform is `KALSHI`) | | `marketTickers` | `string[] \| undefined` | Kalshi market tickers (present when platform is `KALSHI`) | | `marketSlug` | `string \| undefined` | Polymarket market slug (present when platform is `POLYMARKET`) | | `tokenIds` | `string[] \| undefined` | Polymarket token IDs (present when platform is `POLYMARKET`) | | `marketId` | `string \| undefined` | Source market ID (present for other platforms) | | `outcomeIds` | `string[] \| undefined` | Source outcome IDs (present for other platforms) | ### GetSportsMatchingMarketsRequest [#getsportsmatchingmarketsrequest] Pass an empty object `{}` to fetch all markets, or provide filter fields. Only one platform-ID filter type may be used per request. | Field | Type | Description | | ---------------------- | --------------------- | ------------------------------------------------------------------------------------ | | `limit` | `number` | Results per page (1–100, default 25). Ignored in lookup mode. | | `cursor` | `string` | Cursor from a previous `pagination.nextCursor` | | `includeSettled` | `boolean` | Also return events whose game date has passed (default `false`) | | `includeSubmarkets` | `boolean` | Add canonical event, line, outcome, and exact provider ID mappings (default `false`) | | `eventId` | `string \| string[]` | Canonical event key(s) for exact lookup (max 100 unique values) | | `kalshiEventTicker` | `string \| string[]` | Kalshi event ticker(s) (max 100 unique values) | | `polymarketMarketSlug` | `string \| string[]` | Polymarket market slug(s) (max 100 unique values) | | `predictMarketId` | `string \| string[]` | Predict market ID(s) (max 100 unique values) | | `sxbetMarketId` | `string \| string[]` | SX Bet market ID(s) (max 100 unique values) | | `alphaArcadeMarketId` | `string \| string[]` | AlphaArcade market ULID(s) (max 100 unique values) | | `prophetxEventId` | `string \| string[]` | ProphetX event ID(s) (max 100 unique values) | | `prophetxMarketId` | `string \| string[]` | ProphetX market ID(s) `:` (max 100 unique values) | | `playerPropMatch` | `strict \| same_prop` | Player-prop policy; requires `includeSubmarkets: true` (default `strict`) | ```ts // Filter lookup — pagination block omitted from response. const response = await client.getSportsMatchingMarkets({ kalshiEventTicker: "KXNBAGAME-26OCT20OKCSAS", }); // Paginated list — walk every page. let cursor: string | undefined; do { const page = await client.getSportsMatchingMarkets({ limit: 50, cursor }); // ... process page.markets cursor = page.pagination?.nextCursor; } while (cursor); ``` See the [Pagination guide](/guides/pagination) for the full walkthrough. ### MarketsListResponse [#marketslistresponse] The response from `getMarkets`. Contains `data`, catalog `snapshot` freshness, and `pagination`. | Field | Type | Description | | ------------ | ----------------- | ------------------------------------------------------------------------------------------------------ | | `data` | `UnifiedMarket[]` | Array of markets for the current page | | `snapshot` | `MarketsSnapshot` | Catalog freshness; `observedAt` is when the oldest provider snapshot in the bound page scope was built | | `pagination` | `PaginationBlock` | Pagination metadata | ### UnifiedMarket [#unifiedmarket] A market from any supported prediction market provider. | Field | Type | Description | | ---------- | ----------------------- | ---------------------------------------------------------------------------------------- | | `id` | `string` | Composite ID in the format `{provider}:{provider_id}` | | `provider` | `UnifiedMarketProvider` | `kalshi`, `polymarket`, `predict`, `sxbet`, `hyperliquid`, `alpha-arcade`, or `prophetx` | | `title` | `string` | Human-readable market title/question | | `category` | `MarketCategory` | Canonical top-level category, such as `sports`, `politics`, or `crypto` | ### GetMarketsRequest [#getmarketsrequest] | Field | Type | Description | | ---------- | --------------------------- | ---------------------------------------------------------------------- | | `limit` | `number` | Results per page (1–100, default 25) | | `cursor` | `string` | Cursor from a previous `pagination.nextCursor` | | `category` | `MarketCategory` | Optional canonical category filter | | `provider` | `GetMarketsRequestProvider` | Optional canonical provider filter; replay cursors with the same value | ```ts // Paginated list — walk every page. let cursor: string | undefined; do { const page = await client.getMarkets({ limit: 50, cursor }); // ... process page.data cursor = page.pagination?.nextCursor; } while (cursor); ``` ## Raw responses [#raw-responses] Use `.withRawResponse()` to access HTTP metadata alongside the parsed body: ```ts const raw = await client.getSportsMatchingMarkets().withRawResponse(); console.log(raw.rawResponse.status); console.log(raw.rawResponse.headers.get("X-RateLimit-Reset")); // Unix seconds console.log(raw.data.markets); ``` ## Error handling [#error-handling] The SDK throws typed errors for known HTTP error codes: | Error class | Status code | Description | | ------------------------- | ----------- | --------------------------------------------------------------------------------------- | | `BadRequestError` | 400 | Invalid query parameters | | `UnauthorizedError` | 401 | Missing or invalid API key | | `PaymentRequiredError` | 402 | Monthly allowance exhausted or subscription payment recovery required | | `ForbiddenError` | 403 | Insufficient permissions | | `NotFoundError` | 404 | Market or event not found | | `ConflictError` | 409 | Bare identifier resolves on multiple providers; retry with `platform` or a composite ID | | `TooManyRequestsError` | 429 | Rate limit exceeded | | `BadGatewayError` | 502 | Upstream provider or API-key authorization dependency failure | | `ServiceUnavailableError` | 503 | Service temporarily unavailable | Retries prefer `Retry-After` on `429`, then the second-based `X-RateLimit-Reset`. The runtime also accepts legacy millisecond reset values and will not misinterpret them as a 60-second wait. ```ts import { PredictorSDK, PredictorSDKClient } from "@predictorsdk/client"; const client = new PredictorSDKClient({ token: "YOUR_API_KEY", }); try { await client.getSportsMatchingMarkets({ kalshiEventTicker: "KXWNBAGAME-26AUG28TORLV", polymarketMarketSlug: "wnba-tor-las-2026-08-28", }); } catch (error) { if (error instanceof PredictorSDK.BadRequestError) { console.log("Bad request:", error.body.error); } } ``` # Get Binance crypto prices (/api-reference/getBinanceCryptoPrices) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List categories (/api-reference/getCategories) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get event with nested markets (/api-reference/getEvent) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get market detail (/api-reference/getMarket) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List markets (/api-reference/getMarkets) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List public API plans (/api-reference/getPlans) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get Polymarket wallet profile (/api-reference/getPolymarketWallet) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Match sports markets and compare player-prop rules (/api-reference/getSportsMatchingMarkets) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List Polymarket wallet positions (/api-reference/listPolymarketWalletPositions) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}