From c272e27db9ce41e5be31a759bb6b8eadd4aca8d5 Mon Sep 17 00:00:00 2001 From: Jamie pou <99329078+elpou88@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:39:30 +0200 Subject: [PATCH 1/3] fix(suibets): address 4 review findings in SuiBets exchange integration - Restore fetchSeries: false in series fetching calls - Add params.series guard to prevent undefined spread - Remove unused SuiBetsApiResponse import - Add SuiBetsOptions interface and wire walletAddress through TypeScript client --- core/src/exchanges/suibets/api.ts | 2 +- core/src/exchanges/suibets/config.ts | 4 +-- core/src/exchanges/suibets/errors.ts | 14 ++++++++ core/src/exchanges/suibets/fetcher.ts | 43 ++++++++++++++++++++---- core/src/exchanges/suibets/index.ts | 5 ++- core/src/exchanges/suibets/normalizer.ts | 32 ++---------------- sdks/typescript/pmxt/client.ts | 42 +++++++++++++++++++++-- 7 files changed, 98 insertions(+), 44 deletions(-) diff --git a/core/src/exchanges/suibets/api.ts b/core/src/exchanges/suibets/api.ts index 55150eb9..2cfe455d 100644 --- a/core/src/exchanges/suibets/api.ts +++ b/core/src/exchanges/suibets/api.ts @@ -5,7 +5,7 @@ * It is NOT wired into defineImplicitApi — the fetcher calls these endpoints * directly via FetcherContext.http (the rate-limited HTTP client). * - * Base URL: https://suibets.replit.app + * Base URL: https://www.suibets.com * * Endpoints: * GET /api/p2p/offers - List open P2P offers (status, matchId, sport, limit, offset) diff --git a/core/src/exchanges/suibets/config.ts b/core/src/exchanges/suibets/config.ts index d83c8b05..9c5ac180 100644 --- a/core/src/exchanges/suibets/config.ts +++ b/core/src/exchanges/suibets/config.ts @@ -1,4 +1,4 @@ -export const SUIBETS_BASE_URL = 'https://suibets.replit.app'; +export const SUIBETS_BASE_URL = 'https://www.suibets.com'; // SuiBets is a P2P sports betting platform on Sui blockchain. // Platform takes a 2% fee on settled markets. @@ -15,7 +15,7 @@ export const MAX_PRICE = 0.99; export const RATE_LIMIT_MS = 300; // Allowlist of permitted hostnames for SSRF protection -export const ALLOWED_HOSTS: readonly string[] = ['suibets.replit.app']; +export const ALLOWED_HOSTS: readonly string[] = ['www.suibets.com']; /** * Validates that the given URL's hostname is in the ALLOWED_HOSTS allowlist. diff --git a/core/src/exchanges/suibets/errors.ts b/core/src/exchanges/suibets/errors.ts index 8e57f952..45e39ef2 100644 --- a/core/src/exchanges/suibets/errors.ts +++ b/core/src/exchanges/suibets/errors.ts @@ -44,6 +44,20 @@ export class SuibetsErrorMapper extends ErrorMapper { if (axios.isAxiosError(error)) { const status = error.response?.status; + // HTML body = hosting/gateway outage — not a missing resource. + // Return ExchangeNotAvailable so callers can distinguish + // "offer not found" from "upstream server is down". + const responseData = error.response?.data; + const isHtml = + typeof responseData === 'string' && + responseData.trimStart().startsWith('<'); + if (isHtml) { + return new ExchangeNotAvailable( + 'SuiBets API is unavailable. Check https://www.suibets.com for service status.', + this.exchangeName, + ); + } + if (status === 429) { const retryAfter = error.response?.headers?.['retry-after']; const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : undefined; diff --git a/core/src/exchanges/suibets/fetcher.ts b/core/src/exchanges/suibets/fetcher.ts index 6a542045..63a90a82 100644 --- a/core/src/exchanges/suibets/fetcher.ts +++ b/core/src/exchanges/suibets/fetcher.ts @@ -37,6 +37,34 @@ export interface SuibetsRawEvent { offers?: SuibetsRawOffer[]; } + +/** + * Structured return type for fetchRawPositions. + * Keeps the three position-array types separate so each is normalised + * with the correct shape instead of being cast from unknown[]. + */ +export interface SuibetsRawPositions { + createdOffers: SuibetsRawOffer[]; + matchedBets: unknown[]; + parlays: unknown[]; +} + +/** + * Type guard: true only when the value has the core fields that + * SuibetsNormalizer.normalizePosition() reads (id, creatorOdds, creatorStake). + * Guards against silent garbage output when matchedBets or parlays + * are accidentally passed in as SuibetsRawOffer. + */ +export function isSuibetsRawOffer(value: unknown): value is SuibetsRawOffer { + if (typeof value !== 'object' || value === null) return false; + const v = value as Record; + return ( + (typeof v['id'] === 'string' || typeof v['id'] === 'number') && + typeof v['creatorOdds'] === 'number' && + typeof v['creatorStake'] === 'number' + ); +} + export class SuibetsFetcher implements IExchangeFetcher { private readonly ctx: FetcherContext; private readonly baseUrl: string; @@ -179,18 +207,21 @@ export class SuibetsFetcher implements IExchangeFetcher { + async fetchRawPositions(walletAddress: string): Promise { const data = await this.get<{ createdOffers?: unknown[]; matchedBets?: unknown[]; parlays?: unknown[]; }>('/api/p2p/my', { wallet: walletAddress }); - return [ - ...(data.createdOffers ?? []), - ...(data.matchedBets ?? []), - ...(data.parlays ?? []), - ]; + return { + createdOffers: (data.createdOffers ?? []).filter(isSuibetsRawOffer), + matchedBets: data.matchedBets ?? [], + parlays: data.parlays ?? [], + }; } } diff --git a/core/src/exchanges/suibets/index.ts b/core/src/exchanges/suibets/index.ts index 866eb61a..62223dd9 100644 --- a/core/src/exchanges/suibets/index.ts +++ b/core/src/exchanges/suibets/index.ts @@ -9,14 +9,13 @@ import { AuthenticationError } from '../../errors'; import { getSuibetsConfig, SuibetsApiConfig, RATE_LIMIT_MS, validateBaseUrl } from './config'; import { SuibetsFetcher, SuibetsRawOffer } from './fetcher'; import { SuibetsNormalizer } from './normalizer'; -import { suibetsErrorMapper } from './errors'; import { fromOutcomeId } from './utils'; import { FetcherContext } from '../interfaces'; export interface SuibetsCredentials extends ExchangeCredentials { /** Sui wallet address for fetching personal positions */ walletAddress?: string; - /** Override API base URL (default: https://suibets.replit.app) */ + /** Override API base URL (default: https://www.suibets.com) */ baseUrl?: string; } @@ -145,6 +144,6 @@ export class SuiBetsExchange extends PredictionMarketExchange { ); } const raw = await this.fetcher.fetchRawPositions(wallet); - return raw.map(r => this.normalizer.normalizePosition(r as SuibetsRawOffer)); + return raw.createdOffers.map(r => this.normalizer.normalizePosition(r)); } } diff --git a/core/src/exchanges/suibets/normalizer.ts b/core/src/exchanges/suibets/normalizer.ts index 7b9f93c0..3885091e 100644 --- a/core/src/exchanges/suibets/normalizer.ts +++ b/core/src/exchanges/suibets/normalizer.ts @@ -1,7 +1,6 @@ import { IExchangeNormalizer } from '../interfaces'; import { UnifiedMarket, UnifiedEvent, Position } from '../../types'; import { SuibetsRawOffer, SuibetsRawEvent } from './fetcher'; -import { buildSourceMetadata } from '../../utils/metadata'; import { impliedProbability, takerProbability, @@ -12,21 +11,6 @@ import { mapStatus, } from './utils'; -// Raw SuiBets offer fields already promoted to first-class UnifiedMarket columns. -// Omit these from sourceMetadata to capture only vendor-specific data not -// represented by the unified shape. -const SUIBETS_PROMOTED_OFFER_KEYS = [ - 'id', 'matchId', 'matchName', 'homeTeam', 'awayTeam', - 'creatorOdds', 'creatorStake', 'remainingStake', 'totalMatched', - 'matchDate', 'expiresAt', 'status', 'onchainOfferId', - 'leagueName', 'sport', 'isOnchain', -] as const; - -// Raw SuiBets event fields already promoted to first-class UnifiedEvent columns. -const SUIBETS_PROMOTED_EVENT_KEYS = [ - 'id', 'name', 'homeTeam', 'awayTeam', 'sport', 'leagueName', 'offers', -] as const; - function liquidity(offer: SuibetsRawOffer): number { const remaining = offer.remainingStake ?? offer.creatorStake; return mistToSui(remaining); @@ -80,19 +64,13 @@ export class SuibetsNormalizer implements IExchangeNormalizer Boolean(t)), contractAddress: raw.onchainOfferId, yes: creatorOutcome, no: takerOutcome, - // Retains creatorWallet, creatorTeam, takerStake, currency \u2014 fields - // that are vendor-specific and not promoted to any unified column. - sourceMetadata: buildSourceMetadata( - raw as unknown as Record, - SUIBETS_PROMOTED_OFFER_KEYS, - ), }; return market; @@ -122,15 +100,9 @@ export class SuibetsNormalizer implements IExchangeNormalizer Boolean(t)), - // Retains matchDate and status \u2014 event-level fields not promoted to - // any first-class UnifiedEvent column. - sourceMetadata: buildSourceMetadata( - raw as unknown as Record, - SUIBETS_PROMOTED_EVENT_KEYS, - ), }; } diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index 64f2032b..18400d26 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -2821,18 +2821,56 @@ export class Hyperliquid extends Exchange { } } +/** + * Options for the SuiBets exchange client. + */ +export interface SuiBetsOptions extends ExchangeOptions { + /** + * Sui wallet address (0x + 64 hex chars). + * Required for fetchPositions(). Can also be set via the + * SUIBETS_WALLET_ADDRESS environment variable on the sidecar. + */ + walletAddress?: string; +} + /** * SuiBets exchange client. * + * SuiBets is a decentralised P2P sports betting exchange on Sui mainnet. + * No house edge. 2% platform fee. + * Contract: 0xd51fe151bec66a15b086a67c1cfce9b05759ddac1d73fcd3e14324ad202b2e59 + * * @example * ```typescript * const suibets = new SuiBets(); - * const markets = await suibets.fetchMarkets(); + * const markets = await suibets.fetchMarkets({ limit: 20 }); + * + * // With wallet for fetchPositions() + * const me = new SuiBets({ walletAddress: '0xabc...' }); + * const positions = await me.fetchPositions(); * ``` */ export class SuiBets extends Exchange { - constructor(options: ExchangeOptions = {}) { + private readonly _walletAddress?: string; + + constructor(options: SuiBetsOptions = {}) { super("suibets", options); + this._walletAddress = options.walletAddress; + } + + /** + * Includes walletAddress in the credentials sent to the sidecar so + * that fetchPositions() can reach the /api/p2p/my endpoint. + * Falls back to SUIBETS_WALLET_ADDRESS env var on the sidecar side + * when walletAddress is not set here. + */ + protected override getCredentials(): ExchangeCredentials | undefined { + const base = super.getCredentials(); + if (!this._walletAddress) return base; + return { + ...(base ?? {}), + walletAddress: this._walletAddress, + } as ExchangeCredentials & { walletAddress: string }; } } From 57d4043ae749df4d562b03b465d2f0cccdb88043 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Mon, 8 Jun 2026 08:48:23 +0000 Subject: [PATCH 2/3] docs: regenerate API references for suibets review fixes --- core/api-doc-config.generated.json | 140 +++++++++++++++-------------- sdks/python/API_REFERENCE.md | 49 ++++++++++ sdks/typescript/API_REFERENCE.md | 49 ++++++++++ 3 files changed, 173 insertions(+), 65 deletions(-) diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 410e1883..005c6875 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,25 +1,25 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-05-22T19:00:19.281Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:48:08.860Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", - "description": "method: string;\n /** URL path template, relative to the descriptor's baseUrl. */\n path: string;\n /** Whether this endpoint requires authenticated credentials. */\n isPrivate?: boolean;\n /** Identifier used to generate the implicit API method name. */\n operationId?: string;\n /**\nWhen set, requests use this base URL instead of the descriptor default\n(OpenAPI path- or operation-level `servers` override).\n/\n baseUrl?: string;\n}\n\nexport interface ApiDescriptor {\n /** Base URL that all endpoint paths are resolved against. */\n baseUrl: string;\n /** Map of endpoint key to endpoint definition used by the implicit API machinery. */\n endpoints: Record;\n}\n\nexport interface ImplicitApiMethodInfo {\n /** Generated method name exposed on the exchange instance. */\n name: string;\n /** HTTP verb for the underlying endpoint. */\n method: string;\n /** URL path template for the underlying endpoint. */\n path: string;\n /** Whether the underlying endpoint requires authenticated credentials. */\n isPrivate: boolean;\n}\n\nexport interface MarketFilterParams {\n /** Maximum number of results to return */\n limit?: number;\n /** Pagination offset — number of results to skip */\n offset?: number;\n /** Sort order for results */\n sort?: 'volume' | 'liquidity' | 'newest';\n status?: 'active' | 'inactive' | 'closed' | 'all'; // Filter by market status (default: 'active', 'inactive' and 'closed' are interchangeable)\n searchIn?: 'title' | 'description' | 'both'; // Where to search (default: 'title')\n query?: string; // For keyword search\n slug?: string; // For slug/ticker lookup\n marketId?: string; // Direct lookup by market ID\n outcomeId?: string; // Reverse lookup -- find market containing this outcome\n eventId?: string; // Find markets belonging to an event\n page?: number; // For pagination (used by Limitless)\n similarityThreshold?: number; // For semantic search (used by Limitless)\n}\n\nexport interface MarketFetchParams extends MarketFilterParams {\n /** Optional client-side filter applied after fetching */\n filter?: MarketFilterCriteria;\n /** Filter by category. Each market belongs to a venue-assigned category such as \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Filter by tags. Returns markets matching ANY of the provided tags. Tags are more specific than categories -- for example a \"Sports\" market might carry tags [\"Sports\", \"FIFA World Cup\", \"2026 FIFA World Cup\"]. Common tags include \"Crypto\", \"Politics\", \"Elections\", \"Geopolitics\", \"Fed Rates\", \"Trump\". */\n tags?: string[];\n}\n\nexport interface EventFetchParams {\n query?: string; // For keyword search\n /** Maximum number of results to return */\n limit?: number;\n /** Pagination offset — number of results to skip */\n offset?: number;\n /** Sort order for results */\n sort?: 'volume' | 'liquidity' | 'newest';\n status?: 'active' | 'inactive' | 'closed' | 'all'; // Filter by event status (default: 'active', 'inactive' and 'closed' are interchangeable)\n /** Where to search (default: 'title') */\n searchIn?: 'title' | 'description' | 'both';\n eventId?: string; // Direct lookup by event ID\n slug?: string; // Lookup by event slug\n /** Optional client-side filter applied after fetching */\n filter?: EventFilterCriteria;\n /** Filter by category. Each event belongs to a venue-assigned category such as \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Filter by tags. Returns events matching ANY of the provided tags. Tags are more specific than categories -- for example a \"Politics\" event might carry tags [\"Politics\", \"Geopolitics\", \"Middle East\", \"Iran\"]. Common tags include \"Crypto\", \"Elections\", \"Fed Rates\", \"FIFA World Cup\", \"Trump\". */\n tags?: string[];\n}\n\n/**\nDeprecated - use OHLCVParams or TradesParams instead. Resolution is optional for backward compatibility.\n/\nexport interface HistoryFilterParams {\n resolution?: CandleInterval; // Optional for backward compatibility\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return */\n limit?: number;\n}\n\nexport interface OHLCVParams {\n resolution: CandleInterval; // Required for candle aggregation\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return */\n limit?: number;\n}\n\n/**\nParameters for fetching trade history. No resolution parameter - trades are discrete events.\n/\nexport interface TradesParams {\n // No resolution - trades are discrete events, not aggregated\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return */\n limit?: number;\n}\n\nexport interface MyTradesParams {\n outcomeId?: string; // filter to specific outcome/ticker\n marketId?: string; // filter to specific market\n /** Only return records after this date */\n since?: Date;\n /** Only return records before this date */\n until?: Date;\n /** Maximum number of results to return */\n limit?: number;\n cursor?: string; // for Kalshi cursor pagination\n}\n\nexport interface OrderHistoryParams {\n marketId?: string; // required for Limitless (slug)\n /** Only return records after this date */\n since?: Date;\n /** Only return records before this date */\n until?: Date;\n /** Maximum number of results to return */\n limit?: number;\n /** Opaque pagination cursor from a previous response */\n cursor?: string;\n}\n\n// ----------------------------------------------------------------------------\n// Filtering Types\n// ----------------------------------------------------------------------------\n\nexport interface MarketFilterCriteria {\n // Text search\n text?: string;\n searchIn?: ('title' | 'description' | 'category' | 'tags' | 'outcomes')[]; // Default: ['title']\n\n // Numeric range filters\n volume24h?: { min?: number; max?: number };\n /** Filter by total (lifetime) volume range */\n volume?: { min?: number; max?: number };\n /** Filter by current liquidity range */\n liquidity?: { min?: number; max?: number };\n /** Filter by open interest range */\n openInterest?: { min?: number; max?: number };\n\n // Date filters\n resolutionDate?: {\n before?: Date;\n after?: Date;\n };\n\n // Category/tag filters\n /** Filter by category. Common values: \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Match markets that have ANY of these tags. Examples: [\"Crypto\", \"Crypto Prices\"], [\"Politics\", \"Elections\"], [\"Sports\", \"FIFA World Cup\"]. */\n tags?: string[];\n\n // Price filters (for binary markets)\n price?: {\n outcome: 'yes' | 'no' | 'up' | 'down';\n min?: number; // 0.0 to 1.0\n max?: number;\n };\n\n // Price change filters\n priceChange24h?: {\n outcome: 'yes' | 'no' | 'up' | 'down';\n min?: number; // e.g., -0.1 for 10% drop\n max?: number;\n };\n}\n\nexport type MarketFilterFunction = (market: UnifiedMarket) => boolean;\n\nexport interface EventFilterCriteria {\n // Text search\n text?: string;\n searchIn?: ('title' | 'description' | 'category' | 'tags')[]; // Default: ['title']\n\n // Category/tag filters\n /** Filter by category. Common values: \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Match events that have ANY of these tags. Examples: [\"Crypto\"], [\"Politics\", \"Geopolitics\", \"Middle East\"], [\"Sports\", \"FIFA World Cup\"]. */\n tags?: string[];\n\n // Filter by contained markets\n marketCount?: { min?: number; max?: number };\n totalVolume?: { min?: number; max?: number }; // Sum of market volumes\n}\n\nexport type EventFilterFunction = (event: UnifiedEvent) => boolean;\n\n// ----------------------------------------------------------------------------\n// Capability Map (ccxt-style exchange.has)\n// ----------------------------------------------------------------------------\n\nexport type ExchangeCapability = true | false | 'emulated';\n\nexport interface ExchangeHas {\n /** Whether this exchange supports fetching markets. */\n fetchMarkets: ExchangeCapability;\n /** Whether this exchange supports fetching events. */\n fetchEvents: ExchangeCapability;\n /** Whether this exchange supports fetching OHLCV candles. */\n fetchOHLCV: ExchangeCapability;\n /** Whether this exchange supports fetching the order book. */\n fetchOrderBook: ExchangeCapability;\n /** Whether this exchange supports fetching multiple market order books. */\n fetchOrderBooks: ExchangeCapability;\n /** Whether this exchange supports fetching public trades. */\n fetchTrades: ExchangeCapability;\n /** Whether this exchange supports creating orders. */\n createOrder: ExchangeCapability;\n /** Whether this exchange supports cancelling orders. */\n cancelOrder: ExchangeCapability;\n /** Whether this exchange supports fetching a single order by id. */\n fetchOrder: ExchangeCapability;\n /** Whether this exchange supports fetching open orders. */\n fetchOpenOrders: ExchangeCapability;\n /** Whether this exchange supports fetching account positions. */\n fetchPositions: ExchangeCapability;\n /** Whether this exchange supports fetching account balances. */\n fetchBalance: ExchangeCapability;\n /** Whether this exchange supports subscribing to an on-chain address for updates. */\n watchAddress: ExchangeCapability;\n /** Whether this exchange supports unsubscribing from a watched address. */\n unwatchAddress: ExchangeCapability;\n /** Whether this exchange supports streaming order book updates. */\n watchOrderBook: ExchangeCapability;\n /** Whether this exchange supports batch-subscribing to multiple order book streams. */\n watchOrderBooks: ExchangeCapability;\n /** Whether this exchange supports unsubscribing from an order book stream. */\n unwatchOrderBook: ExchangeCapability;\n /** Whether this exchange supports streaming trade updates. */\n watchTrades: ExchangeCapability;\n /** Whether this exchange supports fetching the authenticated user's trade history. */\n fetchMyTrades: ExchangeCapability;\n /** Whether this exchange supports fetching closed orders. */\n fetchClosedOrders: ExchangeCapability;\n /** Whether this exchange supports fetching all orders (open and closed). */\n fetchAllOrders: ExchangeCapability;\n /** Whether this exchange supports building a signed order without submitting it. */\n buildOrder: ExchangeCapability;\n /** Whether this exchange supports submitting a pre-built order. */\n submitOrder: ExchangeCapability;\n /** Whether this exchange supports fetching cross-venue market matches. */\n fetchMarketMatches: ExchangeCapability;\n /** @deprecated Use {@link fetchMarketMatches} instead. */\n fetchMatches: ExchangeCapability;\n /** Whether this exchange supports fetching cross-venue event matches. */\n fetchEventMatches: ExchangeCapability;\n /** Whether this exchange supports comparing prices across venues. */\n compareMarketPrices: ExchangeCapability;\n /** Whether this exchange supports finding related markets across venues. */\n fetchRelatedMarkets: ExchangeCapability;\n /** Whether this exchange supports fetching matched markets across venues. */\n fetchMatchedMarkets: ExchangeCapability;\n /** @deprecated Use {@link fetchMatchedMarkets} instead. */\n fetchMatchedPrices: ExchangeCapability;\n /** @deprecated Use {@link fetchRelatedMarkets} instead. */\n fetchHedges: ExchangeCapability;\n /** @deprecated Use {@link fetchMatchedMarkets} instead. */\n fetchArbitrage: ExchangeCapability;\n}\n\n/**\nOptional authentication credentials for exchange operations.\n/\nexport interface ExchangeCredentials {\n // Standard API authentication (Kalshi, etc.)\n apiKey?: string;\n /** Standard API secret for HMAC-authenticated exchanges */\n apiSecret?: string;\n /** Standard API passphrase for HMAC-authenticated exchanges */\n passphrase?: string;\n /** Metaculus: `Authorization: Token ` for higher rate limits */\n apiToken?: string;\n\n // Blockchain-based authentication (Polymarket)\n privateKey?: string; // Required for Polymarket L1 auth\n\n // Polymarket-specific L2 fields\n signatureType?: number | string; // 0 = EOA, 1 = Poly Proxy, 2 = Gnosis Safe (Can also use 'eoa', 'polyproxy', 'gnosis_safe')\n funderAddress?: string; // The address funding the trades (defaults to signer address)\n\n // Limitless: wallet address for delegated signing profile lookup\n walletAddress?: string;\n\n // Optional base URL override for venue API (e.g., proxy for geo-restricted venues)\n baseUrl?: string;\n}\n\nexport interface ExchangeOptions {\n /**\nHow long (ms) a market snapshot created by `fetchMarketsPaginated` remains valid\nbefore being discarded and re-fetched from the API on the next call.\nDefaults to 0 (no TTL — the snapshot is re-fetched on every initial call).\n/\n snapshotTTL?: number;\n}\n\n/** Shape returned by fetchMarketsPaginated */\nexport interface PaginatedMarketsResult {\n /** The page of unified markets */\n data: UnifiedMarket[];\n /** Total number of markets in the snapshot */\n total: number;\n /** Cursor to pass to the next call, or undefined if this is the last page */\n nextCursor?: string;\n}\n\n/** Shape returned by fetchEventsPaginated */\nexport interface PaginatedEventsResult {\n /** The page of unified events */\n data: UnifiedEvent[];\n /** Total number of events in the snapshot */\n total: number;\n /** Cursor to pass to the next call, or undefined if this is the last page */\n nextCursor?: string;\n}\n\n// ----------------------------------------------------------------------------\n// Base Exchange Class\n// ----------------------------------------------------------------------------\n\nexport abstract class PredictionMarketExchange {\n [key: string]: any; // Allow dynamic method assignment for implicit API\n\n public verbose: boolean = false;\n public http: AxiosInstance;\n public enableRateLimit: boolean = true;\n // Market Cache\n public markets: Record = {};\n public marketsBySlug: Record = {};\n public loadedMarkets: boolean = false;\n /**\nCapability map derived automatically from method overrides at runtime.\nExchanges do NOT need to declare this manually -- if a subclass overrides\na method (and the override does not throw \"not supported\"), it is `true`.\nTo mark a capability as `'emulated'`, add its key to `emulatedCapabilities`.", + "description": "method: string;\n /** URL path template, relative to the descriptor's baseUrl. */\n path: string;\n /** Whether this endpoint requires authenticated credentials. */\n isPrivate?: boolean;\n /** Identifier used to generate the implicit API method name. */\n operationId?: string;\n /**\nWhen set, requests use this base URL instead of the descriptor default\n(OpenAPI path- or operation-level `servers` override).\n/\n baseUrl?: string;\n}\n\nexport interface ApiDescriptor {\n /** Base URL that all endpoint paths are resolved against. */\n baseUrl: string;\n /** Map of endpoint key to endpoint definition used by the implicit API machinery. */\n endpoints: Record;\n}\n\nexport interface ImplicitApiMethodInfo {\n /** Generated method name exposed on the exchange instance. */\n name: string;\n /** HTTP verb for the underlying endpoint. */\n method: string;\n /** URL path template for the underlying endpoint. */\n path: string;\n /** Whether the underlying endpoint requires authenticated credentials. */\n isPrivate: boolean;\n}\n\nexport interface MarketFilterParams {\n /** Maximum number of results to return */\n limit?: number;\n /** Pagination offset — number of results to skip */\n offset?: number;\n /** Sort order for results */\n sort?: 'volume' | 'liquidity' | 'newest';\n status?: 'active' | 'inactive' | 'closed' | 'all'; // Filter by market status (default: 'active', 'inactive' and 'closed' are interchangeable)\n searchIn?: 'title' | 'description' | 'both'; // Where to search (default: 'title')\n query?: string; // For keyword search\n slug?: string; // For slug/ticker lookup\n marketId?: string; // Direct lookup by market ID\n outcomeId?: string; // Reverse lookup -- find market containing this outcome\n eventId?: string; // Find markets belonging to an event\n page?: number; // For pagination (used by Limitless)\n similarityThreshold?: number; // For semantic search (used by Limitless)\n}\n\nexport interface MarketFetchParams extends MarketFilterParams {\n /** Optional client-side filter applied after fetching */\n filter?: MarketFilterCriteria;\n /** Filter by category. Each market belongs to a venue-assigned category such as \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Filter by tags. Returns markets matching ANY of the provided tags. Tags are more specific than categories -- for example a \"Sports\" market might carry tags [\"Sports\", \"FIFA World Cup\", \"2026 FIFA World Cup\"]. Common tags include \"Crypto\", \"Politics\", \"Elections\", \"Geopolitics\", \"Fed Rates\", \"Trump\". */\n tags?: string[];\n}\n\nexport interface EventFetchParams {\n query?: string; // For keyword search\n /** Maximum number of results to return */\n limit?: number;\n /** Opaque venue pagination cursor, where supported. */\n cursor?: string;\n /** Pagination offset — number of results to skip */\n offset?: number;\n /** Sort order for results */\n sort?: 'volume' | 'liquidity' | 'newest';\n status?: 'active' | 'inactive' | 'closed' | 'all'; // Filter by event status (default: 'active', 'inactive' and 'closed' are interchangeable)\n /** Where to search (default: 'title') */\n searchIn?: 'title' | 'description' | 'both';\n eventId?: string; // Direct lookup by event ID\n slug?: string; // Lookup by event slug\n /** Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi `\"KXATPMATCH\"`, Polymarket `\"wta\"`). Passed through to the vendor where supported, otherwise applied to `sourceMetadata` after fetch. */\n series?: string;\n /** Optional client-side filter applied after fetching */\n filter?: EventFilterCriteria;\n /** Filter by category. Each event belongs to a venue-assigned category such as \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Filter by tags. Returns events matching ANY of the provided tags. Tags are more specific than categories -- for example a \"Politics\" event might carry tags [\"Politics\", \"Geopolitics\", \"Middle East\", \"Iran\"]. Common tags include \"Crypto\", \"Elections\", \"Fed Rates\", \"FIFA World Cup\", \"Trump\". */\n tags?: string[];\n}\n\n/**\nParameters for `fetchSeries`. Venues that don't expose a series concept\nreturn an empty array regardless of the filters.\n/\nexport interface SeriesFetchParams {\n /** Direct lookup by venue-native series id (e.g. \"KXATPMATCH\" on Kalshi, \"atp\" or \"1\" on Polymarket Gamma). When set, the result is the matching series with its events populated where the venue supports it. */\n id?: string;\n /** Lookup by series slug (e.g. \"wta\", \"nfl\"). */\n slug?: string;\n /** Keyword search across series title / description. */\n query?: string;\n /** Filter by recurrence cadence ('daily', 'weekly', 'annual', ...). */\n recurrence?: string;\n /** Maximum number of results to return. */\n limit?: number;\n /** Pagination offset. */\n offset?: number;\n}\n\n/**\nDeprecated - use OHLCVParams or TradesParams instead. Resolution is optional for backward compatibility.\n/\nexport interface HistoryFilterParams {\n resolution?: CandleInterval; // Optional for backward compatibility\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return */\n limit?: number;\n}\n\nexport interface OHLCVParams {\n resolution: CandleInterval; // Required for candle aggregation\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return */\n limit?: number;\n}\n\n/**\nParameters for fetching trade history. No resolution parameter - trades are discrete events.\n/\n/** Maximum allowed value for `TradesParams.limit`. */\nexport const MAX_TRADES_LIMIT = 1000;\n\nexport interface TradesParams {\n // No resolution - trades are discrete events, not aggregated\n /** Start of the time range */\n start?: Date;\n /** End of the time range */\n end?: Date;\n /** Maximum number of results to return (max {@link MAX_TRADES_LIMIT}) */\n limit?: number;\n}\n\nexport interface MyTradesParams {\n outcomeId?: string; // filter to specific outcome/ticker\n marketId?: string; // filter to specific market\n /** Only return records after this date */\n since?: Date;\n /** Only return records before this date */\n until?: Date;\n /** Maximum number of results to return */\n limit?: number;\n cursor?: string; // for Kalshi cursor pagination\n}\n\nexport interface FetchOrderBookParams {\n /** Outcome side: 'yes' or 'no'. Required for exchanges like Limitless\n where the API returns a single orderbook per market. */\n side?: 'yes' | 'no';\n /** Outcome alias: 'yes' or 'no', or an outcome token ID. When set,\n the first argument is treated as a market ID and this value selects\n which outcome's order book to fetch. Accepts the literal strings\n 'yes'/'no' (resolved via a market lookup) or a raw outcome token ID. */\n outcome?: string;\n /** Unix timestamp (ms) — fetch a historical snapshot at or before this\n time, or the start of a range when combined with `until` (hosted API only). */\n since?: number;\n /** Unix timestamp (ms) — end of a historical range. When combined with\n `since`, returns an array of reconstructed L2 OrderBook snapshots\n between `since` and `until` (hosted API only). */\n until?: number;\n}\n\nexport interface OrderHistoryParams {\n marketId?: string; // required for Limitless (slug)\n /** Only return records after this date */\n since?: Date;\n /** Only return records before this date */\n until?: Date;\n /** Maximum number of results to return */\n limit?: number;\n /** Opaque pagination cursor from a previous response */\n cursor?: string;\n}\n\n// ----------------------------------------------------------------------------\n// Filtering Types\n// ----------------------------------------------------------------------------\n\nexport interface MarketFilterCriteria {\n // Text search\n text?: string;\n searchIn?: ('title' | 'description' | 'category' | 'tags' | 'outcomes')[]; // Default: ['title']\n\n // Numeric range filters\n volume24h?: { min?: number; max?: number };\n /** Filter by total (lifetime) volume range */\n volume?: { min?: number; max?: number };\n /** Filter by current liquidity range */\n liquidity?: { min?: number; max?: number };\n /** Filter by open interest range */\n openInterest?: { min?: number; max?: number };\n\n // Date filters\n resolutionDate?: {\n before?: Date;\n after?: Date;\n };\n\n // Category/tag filters\n /** Filter by category. Common values: \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Match markets that have ANY of these tags. Examples: [\"Crypto\", \"Crypto Prices\"], [\"Politics\", \"Elections\"], [\"Sports\", \"FIFA World Cup\"]. */\n tags?: string[];\n\n // Price filters (for binary markets)\n price?: {\n outcome: 'yes' | 'no' | 'up' | 'down';\n min?: number; // 0.0 to 1.0\n max?: number;\n };\n\n // Price change filters\n priceChange24h?: {\n outcome: 'yes' | 'no' | 'up' | 'down';\n min?: number; // e.g., -0.1 for 10% drop\n max?: number;\n };\n}\n\nexport type MarketFilterFunction = (market: UnifiedMarket) => boolean;\n\nexport interface EventFilterCriteria {\n // Text search\n text?: string;\n searchIn?: ('title' | 'description' | 'category' | 'tags')[]; // Default: ['title']\n\n // Category/tag filters\n /** Filter by category. Common values: \"Sports\", \"Politics\", \"Crypto\", \"Bitcoin\", \"Soccer\", \"Economic Policy\" (Polymarket) or \"Sports\", \"Mentions\" (Kalshi). */\n category?: string;\n /** Match events that have ANY of these tags. Examples: [\"Crypto\"], [\"Politics\", \"Geopolitics\", \"Middle East\"], [\"Sports\", \"FIFA World Cup\"]. */\n tags?: string[];\n\n // Filter by contained markets\n marketCount?: { min?: number; max?: number };\n totalVolume?: { min?: number; max?: number }; // Sum of market volumes\n}\n\nexport type EventFilterFunction = (event: UnifiedEvent) => boolean;\n\n// ----------------------------------------------------------------------------\n// Capability Map (ccxt-style exchange.has)\n// ----------------------------------------------------------------------------\n\nexport type ExchangeCapability = true | false | 'emulated';\n\nexport interface ExchangeHas {\n /** Whether this exchange supports fetching markets. */\n fetchMarkets: ExchangeCapability;\n /** Whether this exchange supports fetching events. */\n fetchEvents: ExchangeCapability;\n /** Whether this exchange exposes a recurring-series concept (Series -> Event -> Market -> Outcome). Venues without one return `false` and an empty array from `fetchSeries`. */\n fetchSeries: ExchangeCapability;\n /** Whether this exchange supports fetching OHLCV candles. */\n fetchOHLCV: ExchangeCapability;\n /** Whether this exchange supports fetching the order book. */\n fetchOrderBook: ExchangeCapability;\n /** Whether this exchange supports fetching multiple market order books. */\n fetchOrderBooks: ExchangeCapability;\n /** Whether this exchange supports fetching public trades. */\n fetchTrades: ExchangeCapability;\n /** Whether this exchange supports creating orders. */\n createOrder: ExchangeCapability;\n /** Whether this exchange supports cancelling orders. */\n cancelOrder: ExchangeCapability;\n /** Whether this exchange supports fetching a single order by id. */\n fetchOrder: ExchangeCapability;\n /** Whether this exchange supports fetching open orders. */\n fetchOpenOrders: ExchangeCapability;\n /** Whether this exchange supports fetching account positions. */\n fetchPositions: ExchangeCapability;\n /** Whether this exchange supports fetching account balances. */\n fetchBalance: ExchangeCapability;\n /** Whether this exchange supports subscribing to an on-chain address for updates. */\n watchAddress: ExchangeCapability;\n /** Whether this exchange supports unsubscribing from a watched address. */\n unwatchAddress: ExchangeCapability;\n /** Whether this exchange supports streaming order book updates. */\n watchOrderBook: ExchangeCapability;\n /** Whether this exchange supports batch-subscribing to multiple order book streams. */\n watchOrderBooks: ExchangeCapability;\n /** Whether this exchange supports unsubscribing from an order book stream. */\n unwatchOrderBook: ExchangeCapability;\n /** Whether this exchange supports streaming trade updates. */\n watchTrades: ExchangeCapability;\n /** Whether this exchange supports fetching the authenticated user's trade history. */\n fetchMyTrades: ExchangeCapability;\n /** Whether this exchange supports fetching closed orders. */\n fetchClosedOrders: ExchangeCapability;\n /** Whether this exchange supports fetching all orders (open and closed). */\n fetchAllOrders: ExchangeCapability;\n /** Whether this exchange supports building a signed order without submitting it. */\n buildOrder: ExchangeCapability;\n /** Whether this exchange supports submitting a pre-built order. */\n submitOrder: ExchangeCapability;\n /** Whether this exchange supports fetching cross-venue market matches. */\n fetchMarketMatches: ExchangeCapability;\n /** @deprecated Use {@link fetchMarketMatches} instead. */\n fetchMatches: ExchangeCapability;\n /** Whether this exchange supports fetching cross-venue event matches. */\n fetchEventMatches: ExchangeCapability;\n /** Whether this exchange supports comparing prices across venues. */\n compareMarketPrices: ExchangeCapability;\n /** Whether this exchange supports finding related markets across venues. */\n fetchRelatedMarkets: ExchangeCapability;\n /** Whether this exchange supports fetching matched markets across venues. */\n fetchMatchedMarkets: ExchangeCapability;\n /** @deprecated Use {@link fetchMatchedMarkets} instead. */\n fetchMatchedPrices: ExchangeCapability;\n /** @deprecated Use {@link fetchRelatedMarkets} instead. */\n fetchHedges: ExchangeCapability;\n /** @deprecated Use {@link fetchMatchedMarkets} instead. */\n fetchArbitrage: ExchangeCapability;\n}\n\n/**\nOptional authentication credentials for exchange operations.\n/\nexport interface ExchangeCredentials {\n // Standard API authentication (Kalshi, etc.)\n apiKey?: string;\n /** Standard API secret for HMAC-authenticated exchanges */\n apiSecret?: string;\n /** Standard API passphrase for HMAC-authenticated exchanges */\n passphrase?: string;\n /** Metaculus: `Authorization: Token ` for higher rate limits */\n apiToken?: string;\n\n // Blockchain-based authentication (Polymarket)\n privateKey?: string; // Required for Polymarket L1 auth\n\n // Polymarket-specific L2 fields\n signatureType?: number | string; // 0 = EOA, 1 = Poly Proxy, 2 = Gnosis Safe (Can also use 'eoa', 'polyproxy', 'gnosis_safe')\n funderAddress?: string; // The address funding the trades (defaults to signer address)\n\n // Limitless: wallet address for delegated signing profile lookup\n walletAddress?: string;\n\n // Optional base URL override for venue API (e.g., proxy for geo-restricted venues)\n baseUrl?: string;\n}\n\nexport interface ExchangeOptions {\n /**\nHow long (ms) a market snapshot created by `fetchMarketsPaginated` remains valid\nbefore being discarded and re-fetched from the API on the next call.\nDefaults to 0 (no TTL — the snapshot is re-fetched on every initial call).\n/\n snapshotTTL?: number;\n}\n\n/** Shape returned by fetchMarketsPaginated */\nexport interface PaginatedMarketsResult {\n /** The page of unified markets */\n data: UnifiedMarket[];\n /** Total number of markets in the snapshot */\n total: number;\n /** Cursor to pass to the next call, or undefined if this is the last page */\n nextCursor?: string;\n}\n\n/** Shape returned by fetchEventsPaginated */\nexport interface PaginatedEventsResult {\n /** The page of unified events */\n data: UnifiedEvent[];\n /** Total number of events in the snapshot */\n total: number;\n /** Cursor to pass to the next call, or undefined if this is the last page */\n nextCursor?: string;\n}\n\n// ----------------------------------------------------------------------------\n// Base Exchange Class\n// ----------------------------------------------------------------------------\n\nexport abstract class PredictionMarketExchange {\n [key: string]: any; // Allow dynamic method assignment for implicit API\n\n public verbose: boolean = false;\n public http: AxiosInstance;\n public enableRateLimit: boolean = true;\n // Market Cache\n public markets: Record = {};\n public marketsBySlug: Record = {};\n public loadedMarkets: boolean = false;\n /**\nCapability map derived automatically from method overrides at runtime.\nExchanges do NOT need to declare this manually -- if a subclass overrides\na method (and the override does not throw \"not supported\"), it is `true`.\nTo mark a capability as `'emulated'`, add its key to `emulatedCapabilities`.", "params": [], "returns": { "type": "ExchangeHas", "description": "Result" }, - "source": "BaseExchange.ts:40" + "source": "BaseExchange.ts:42" }, "implicitApi": { "summary": "Override in subclasses to force specific capability values.", - "description": "Use `'emulated'` for methods backed by a non-native mechanism,\nor `false` for methods that override the base only to throw a\nbetter error message (e.g. \"pari-mutuel bets cannot be cancelled\").\n/\n protected readonly capabilityOverrides: Partial> = {};\n\n protected credentials?: ExchangeCredentials;\n // Implicit API (merged across multiple defineImplicitApi calls)\n protected apiDescriptor?: ApiDescriptor;\n private _throttler: Throttler;\n // Snapshot state for cursor-based pagination\n private _snapshotTTL: number;\n private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string };\n private _eventSnapshot?: { events: UnifiedEvent[]; takenAt: number; id: string };\n private apiDescriptors: ApiDescriptor[] = [];\n\n constructor(credentials?: ExchangeCredentials, options?: ExchangeOptions) {\n this.credentials = credentials;\n this._snapshotTTL = options?.snapshotTTL ?? 0;\n this.http = axios.create({\n headers: {\n 'User-Agent': `pmxt (https://github.com/pmxt-dev/pmxt)`\n },\n paramsSerializer: {\n serialize: (params) => {\n const sp = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (Array.isArray(v)) v.forEach((x) => sp.append(k, String(x)));\n else sp.append(k, String(v));\n }\n return sp.toString();\n },\n },\n });\n this._throttler = new Throttler({\n refillRate: 1 / this._rateLimit,\n capacity: 1,\n delay: 1,\n });\n\n // Rate Limit Interceptor\n this.http.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {\n if (this.enableRateLimit) {\n await this._throttler.throttle();\n }\n return config;\n });\n\n // Request Interceptor\n this.http.interceptors.request.use((config: InternalAxiosRequestConfig) => {\n if (this.verbose) {\n console.log(`\\n[pmxt] → ${config.method?.toUpperCase()} ${config.url}`);\n if (config.params) console.log('[pmxt] params:', config.params);\n if (config.data) console.log('[pmxt] body:', JSON.stringify(config.data, null, 2));\n }\n return config;\n });\n\n // Response Interceptor\n this.http.interceptors.response.use(\n (response: AxiosResponse) => {\n if (this.verbose) {\n console.log(`\\n[pmxt] ← ${response.status} ${response.statusText} ${response.config.url}`);\n // console.log('[pmxt] response:', JSON.stringify(response.data, null, 2));\n // Commented out full body log to avoid spam, but headers might be useful\n }\n return response;\n },\n (error: any) => {\n if (this.verbose) {\n console.log(`\\n[pmxt] ✖ REQUEST FAILED: ${error.config?.url}`);\n console.log('[pmxt] error:', error.message);\n if (error.response) {\n console.log('[pmxt] status:', error.response.status);\n console.log('[pmxt] data:', JSON.stringify(error.response.data, null, 2));\n }\n }\n return Promise.reject(error);\n }\n );\n }\n\n private _rateLimit: number = 1000;\n\n get rateLimit(): number {\n return this._rateLimit;\n }\n\n set rateLimit(value: number) {\n this._rateLimit = value;\n this._throttler = new Throttler({\n refillRate: 1 / value,\n capacity: 1,\n delay: 1,\n });\n }\n\n abstract get name(): string;\n\n /**\nIntrospection getter: returns info about all implicit API methods.", + "description": "Use `'emulated'` for methods backed by a non-native mechanism,\nor `false` for methods that override the base only to throw a\nbetter error message (e.g. \"pari-mutuel bets cannot be cancelled\").\n/\n protected readonly capabilityOverrides: Partial> = {};\n\n protected credentials?: ExchangeCredentials;\n // Implicit API (merged across multiple defineImplicitApi calls)\n protected apiDescriptor?: ApiDescriptor;\n private _throttler: Throttler;\n // Snapshot state for cursor-based pagination\n private _snapshotTTL: number;\n private _snapshot?: { markets: UnifiedMarket[]; takenAt: number; id: string };\n private _eventSnapshot?: { events: UnifiedEvent[]; takenAt: number; id: string };\n private apiDescriptors: ApiDescriptor[] = [];\n\n constructor(credentials?: ExchangeCredentials, options?: ExchangeOptions) {\n this.credentials = credentials;\n this._snapshotTTL = options?.snapshotTTL ?? 0;\n this.http = axios.create({\n headers: {\n 'User-Agent': `pmxt (https://github.com/pmxt-dev/pmxt)`\n },\n paramsSerializer: {\n serialize: (params) => {\n const sp = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) continue;\n if (Array.isArray(v)) v.forEach((x) => sp.append(k, String(x)));\n else sp.append(k, String(v));\n }\n return sp.toString();\n },\n },\n });\n this._throttler = new Throttler({\n refillRate: 1 / this._rateLimit,\n capacity: 1,\n delay: 1,\n });\n\n // Rate Limit Interceptor\n this.http.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {\n if (this.enableRateLimit) {\n await this._throttler.throttle();\n }\n return config;\n });\n\n // Request Interceptor\n this.http.interceptors.request.use((config: InternalAxiosRequestConfig) => {\n if (this.verbose) {\n logger.debug(`-> ${config.method?.toUpperCase()} ${config.url}`);\n if (config.params) logger.debug('params:', { params: config.params });\n if (config.data) logger.debug('body:', { body: config.data });\n }\n return config;\n });\n\n // Response Interceptor\n this.http.interceptors.response.use(\n (response: AxiosResponse) => {\n if (this.verbose) {\n logger.debug(`<- ${response.status} ${response.statusText} ${response.config.url}`);\n }\n return response;\n },\n (error: any) => {\n if (this.verbose) {\n logger.debug(`REQUEST FAILED: ${error.config?.url}`, {\n error: error.message,\n status: error.response?.status,\n data: error.response?.data,\n });\n }\n return Promise.reject(error);\n }\n );\n }\n\n private _rateLimit: number = 1000;\n\n get rateLimit(): number {\n return this._rateLimit;\n }\n\n set rateLimit(value: number) {\n this._rateLimit = value;\n this._throttler = new Throttler({\n refillRate: 1 / value,\n capacity: 1,\n delay: 1,\n });\n }\n\n abstract get name(): string;\n\n /**\nIntrospection getter: returns info about all implicit API methods.", "params": [], "returns": { "type": "ImplicitApiMethodInfo[]", "description": "Result" }, - "source": "BaseExchange.ts:403" + "source": "BaseExchange.ts:451" }, "loadMarkets": { "summary": "Load and cache all markets from the exchange into `this.markets` and `this.marketsBySlug`.", @@ -36,7 +36,7 @@ "type": "Record", "description": "Dictionary of markets indexed by marketId" }, - "source": "BaseExchange.ts:519" + "source": "BaseExchange.ts:564" }, "fetchMarkets": { "summary": "Fetch markets with optional filtering, search, or slug lookup.", @@ -83,7 +83,7 @@ "ordering — exchanges may reorder or add markets between requests. For stable iteration\nacross pages, use `loadMarkets()` and paginate over `Object.values(exchange.markets)`.", "Some exchanges (like Limitless) may only support status 'active' for search results." ], - "source": "BaseExchange.ts:556" + "source": "BaseExchange.ts:601" }, "fetchMarketsPaginated": { "summary": "Fetch markets with cursor-based pagination backed by a stable in-memory snapshot.", @@ -110,7 +110,7 @@ "type": "PaginatedMarketsResult", "description": "PaginatedMarketsResult with data, total, and optional nextCursor" }, - "source": "BaseExchange.ts:605" + "source": "BaseExchange.ts:650" }, "fetchEventsPaginated": { "summary": "Paginated variant of {@link fetchEvents}.", @@ -137,7 +137,7 @@ "type": "PaginatedEventsResult", "description": "PaginatedEventsResult with data, total, and optional nextCursor" }, - "source": "BaseExchange.ts:674" + "source": "BaseExchange.ts:719" }, "fetchEvents": { "summary": "Fetch events with optional keyword search.", @@ -175,7 +175,24 @@ "notes": [ "Some exchanges (like Limitless) may only support status 'active' for search results." ], - "source": "BaseExchange.ts:743" + "source": "BaseExchange.ts:788" + }, + "fetchSeries": { + "summary": "Fetch the recurring series (fourth tier above Event -> Market -> Outcome)", + "description": "that this venue exposes. Returns an empty array on venues without a\nseries concept (Limitless, Smarkets, Probable, Metaculus, Baozi,\nHyperliquid, SuiBets, Polymarket US).\n\n- `params.id` -> a single matching series with its events populated where supported.\n- no params -> the full list, typically without nested events for payload size.", + "params": [ + { + "name": "params", + "type": "SeriesFetchParams", + "optional": true, + "description": "params" + } + ], + "returns": { + "type": "UnifiedSeries[]", + "description": "Array of unified series. Always an array, including the singular-lookup case." + }, + "source": "BaseExchange.ts:823" }, "fetchMarket": { "summary": "Fetch a single market by lookup parameters.", @@ -192,7 +209,7 @@ "type": "UnifiedMarket", "description": "A single unified market" }, - "source": "BaseExchange.ts:778" + "source": "BaseExchange.ts:841" }, "fetchEvent": { "summary": "Fetch a single event by lookup parameters.", @@ -209,7 +226,7 @@ "type": "UnifiedEvent", "description": "A single unified event" }, - "source": "BaseExchange.ts:809" + "source": "BaseExchange.ts:941" }, "fetchOHLCV": { "summary": "Fetch historical OHLCV (candlestick) price data for a specific market outcome.", @@ -237,11 +254,11 @@ "Polymarket: outcomeId is the CLOB Token ID. Kalshi: outcomeId is the Market Ticker.", "Common resolutions: '1m' | '5m' | '15m' | '1h' | '6h' | '1d'. Arbitrary intervals (e.g. '30s', '120s', '3h') accepted by venues that support them." ], - "source": "BaseExchange.ts:826" + "source": "BaseExchange.ts:958" }, "fetchOrderBook": { "summary": "Fetch the order book (bids/asks) for a specific outcome.", - "description": "Fetch the order book (bids/asks) for a specific outcome.", + "description": "Supports live and historical queries. For historical data, pass `since`\nto get a single snapshot, or `since` + `until` to get an array of fully\nreconstructed L2 books from the archive. Range queries return up to\n`limit` snapshots (default 100, max 1000).", "params": [ { "name": "outcomeId", @@ -253,11 +270,11 @@ "name": "limit", "type": "number", "optional": true, - "description": "Max number of bid/ask levels to return (CCXT-style)." + "description": "Max number of bid/ask levels to return. For range" }, { "name": "params", - "type": "Record", + "type": "FetchOrderBookParams", "optional": true, "description": "Optional parameters:" } @@ -266,7 +283,7 @@ "type": "OrderBook", "description": "Order book with bids and asks. Returns OrderBook[] when" }, - "source": "BaseExchange.ts:841" + "source": "BaseExchange.ts:973" }, "fetchOrderBooks": { "summary": "Batch variant of {@link fetchOrderBook}. Fetches order books for", @@ -283,7 +300,7 @@ "type": "Record", "description": "A map keyed by the input id (preserving the caller's exact" }, - "source": "BaseExchange.ts:863" + "source": "BaseExchange.ts:1001" }, "fetchTrades": { "summary": "Fetch raw trade history for a specific outcome.", @@ -309,7 +326,7 @@ "notes": [ "Polymarket requires an API key for trade history. Use fetchOHLCV for public historical data." ], - "source": "BaseExchange.ts:876" + "source": "BaseExchange.ts:1014" }, "createOrder": { "summary": "Place a new order on the exchange.", @@ -326,7 +343,7 @@ "type": "Order", "description": "The created order" }, - "source": "BaseExchange.ts:896" + "source": "BaseExchange.ts:1031" }, "buildOrder": { "summary": "Build an order payload without submitting it to the exchange.", @@ -343,7 +360,7 @@ "type": "BuiltOrder", "description": "A BuiltOrder containing the exchange-native payload" }, - "source": "BaseExchange.ts:910" + "source": "BaseExchange.ts:1045" }, "submitOrder": { "summary": "Submit a pre-built order returned by buildOrder().", @@ -360,7 +377,7 @@ "type": "Order", "description": "The submitted order" }, - "source": "BaseExchange.ts:922" + "source": "BaseExchange.ts:1057" }, "cancelOrder": { "summary": "Cancel an existing open order.", @@ -377,7 +394,7 @@ "type": "Order", "description": "The cancelled order" }, - "source": "BaseExchange.ts:932" + "source": "BaseExchange.ts:1067" }, "fetchOrder": { "summary": "Fetch a specific order by ID.", @@ -394,7 +411,7 @@ "type": "Order", "description": "The order details" }, - "source": "BaseExchange.ts:942" + "source": "BaseExchange.ts:1077" }, "fetchOpenOrders": { "summary": "Fetch all open orders, optionally filtered by market.", @@ -411,7 +428,7 @@ "type": "Order[]", "description": "Array of open orders" }, - "source": "BaseExchange.ts:952" + "source": "BaseExchange.ts:1087" }, "fetchPositions": { "summary": "Fetch current user positions across all markets.", @@ -428,7 +445,7 @@ "type": "Position[]", "description": "Array of user positions" }, - "source": "BaseExchange.ts:974" + "source": "BaseExchange.ts:1109" }, "fetchBalance": { "summary": "Fetch account balances.", @@ -445,7 +462,7 @@ "type": "Balance[]", "description": "Array of account balances" }, - "source": "BaseExchange.ts:984" + "source": "BaseExchange.ts:1119" }, "getExecutionPrice": { "summary": "Calculate the volume-weighted average execution price for a given order size.", @@ -474,7 +491,7 @@ "type": "number", "description": "Average execution price, or 0 if insufficient liquidity" }, - "source": "BaseExchange.ts:994" + "source": "BaseExchange.ts:1129" }, "getExecutionPriceDetailed": { "summary": "Calculate detailed execution price information including partial fill data.", @@ -503,7 +520,7 @@ "type": "ExecutionPriceResult", "description": "Detailed execution result with price, filled amount, and fill status" }, - "source": "BaseExchange.ts:1007" + "source": "BaseExchange.ts:1142" }, "filterMarkets": { "summary": "Filter a list of markets by criteria.", @@ -526,7 +543,7 @@ "type": "UnifiedMarket[]", "description": "Filtered array of markets" }, - "source": "BaseExchange.ts:1023" + "source": "BaseExchange.ts:1158" }, "filterEvents": { "summary": "Filter a list of events by criteria.", @@ -549,7 +566,7 @@ "type": "UnifiedEvent[]", "description": "Filtered array of events" }, - "source": "BaseExchange.ts:1182" + "source": "BaseExchange.ts:1317" }, "watchOrderBook": { "summary": "Watch order book updates in real-time via WebSocket.", @@ -578,7 +595,7 @@ "type": "OrderBook", "description": "Promise that resolves with the current orderbook state" }, - "source": "BaseExchange.ts:1278" + "source": "BaseExchange.ts:1413" }, "watchOrderBooks": { "summary": "Watch multiple order books simultaneously via WebSocket.", @@ -607,7 +624,7 @@ "type": "Record", "description": "Promise that resolves with order books keyed by ID" }, - "source": "BaseExchange.ts:1291" + "source": "BaseExchange.ts:1426" }, "unwatchOrderBook": { "summary": "Unsubscribe from a previously watched order book stream.", @@ -624,7 +641,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1319" + "source": "BaseExchange.ts:1454" }, "watchTrades": { "summary": "Watch trade executions in real-time via WebSocket.", @@ -659,7 +676,7 @@ "type": "Trade[]", "description": "Promise that resolves with recent trades" }, - "source": "BaseExchange.ts:1332" + "source": "BaseExchange.ts:1467" }, "watchAddress": { "summary": "Stream activity for a public wallet address", @@ -682,7 +699,7 @@ "type": "SubscribedAddressSnapshot", "description": "Promise that resolves with the latest SubscribedAddressSnapshot snapshot" }, - "source": "BaseExchange.ts:1346" + "source": "BaseExchange.ts:1481" }, "unwatchAddress": { "summary": "Stop watching a previously registered wallet address and release its resource updates.", @@ -699,24 +716,17 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1359" + "source": "BaseExchange.ts:1494" }, - "testDummyMethod": { + "close": { "summary": "Close all WebSocket connections and clean up resources.", - "description": "Call this when you're done streaming to properly release connections.\n/\n\n /**\nTest method for auto-generation verification.", - "params": [ - { - "name": "param", - "type": "string", - "optional": true, - "description": "param" - } - ], + "description": "Call this when you're done streaming to properly release connections.", + "params": [], "returns": { - "type": "string", + "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1368" + "source": "BaseExchange.ts:1503" }, "fetchMarketMatches": { "summary": "Find the same or related market on other venues. Two modes:", @@ -733,7 +743,7 @@ "type": "MatchResult[]", "description": "Array of matched markets with relation and confidence" }, - "source": "BaseExchange.ts:1389" + "source": "BaseExchange.ts:1517" }, "fetchMatches": { "summary": "fetchMatches", @@ -750,7 +760,7 @@ "type": "MatchResult[]", "description": "Result" }, - "source": "BaseExchange.ts:1405" + "source": "BaseExchange.ts:1533" }, "fetchEventMatches": { "summary": "Find the same or related event on other venues. Two modes:", @@ -767,7 +777,7 @@ "type": "EventMatchResult[]", "description": "Array of matched events with market-level match details" }, - "source": "BaseExchange.ts:1413" + "source": "BaseExchange.ts:1541" }, "compareMarketPrices": { "summary": "Compare live prices for the same market across venues. Finds identity matches and returns side-by-side best bid/ask prices so you can spot price differences at a glance.", @@ -784,7 +794,7 @@ "type": "PriceComparison[]", "description": "Array of price comparisons across venues" }, - "source": "BaseExchange.ts:1429" + "source": "BaseExchange.ts:1557" }, "fetchRelatedMarkets": { "summary": "Find related markets across venues. Discovers subset/superset market relationships", @@ -801,7 +811,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1439" + "source": "BaseExchange.ts:1567" }, "fetchMatchedMarkets": { "summary": "fetchMatchedMarkets", @@ -818,7 +828,7 @@ "type": "MatchedMarketPair[]", "description": "Result" }, - "source": "BaseExchange.ts:1450" + "source": "BaseExchange.ts:1578" }, "fetchMatchedPrices": { "summary": "fetchMatchedPrices", @@ -835,7 +845,7 @@ "type": "MatchedPricePair[]", "description": "Array of matched market pairs with prices from each venue" }, - "source": "BaseExchange.ts:1458" + "source": "BaseExchange.ts:1586" }, "fetchHedges": { "summary": "fetchHedges", @@ -852,7 +862,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1469" + "source": "BaseExchange.ts:1597" }, "fetchArbitrage": { "summary": "fetchArbitrage", @@ -869,7 +879,7 @@ "type": "ArbitrageOpportunity[]", "description": "Array of arbitrage opportunities sorted by spread" }, - "source": "BaseExchange.ts:1479" + "source": "BaseExchange.ts:1607" }, "watchPrices": { "summary": "Watch AMM price updates for a market address (Limitless only).", @@ -893,7 +903,7 @@ "description": "Result" }, "exchangeOnly": "limitless", - "source": "index.ts:466" + "source": "index.ts:488" }, "watchUserPositions": { "summary": "Watch user positions in real-time (Limitless only).", @@ -911,7 +921,7 @@ "description": "Result" }, "exchangeOnly": "limitless", - "source": "index.ts:478" + "source": "index.ts:500" }, "watchUserTransactions": { "summary": "Watch user transactions in real-time (Limitless only).", @@ -929,7 +939,7 @@ "description": "Result" }, "exchangeOnly": "limitless", - "source": "index.ts:490" + "source": "index.ts:512" }, "initAuth": { "summary": "Initialize L2 API credentials for implicit API signing.", @@ -940,7 +950,7 @@ "description": "Result" }, "exchangeOnly": "polymarket", - "source": "index.ts:136" + "source": "index.ts:143" }, "preWarmMarket": { "summary": "Pre-warm the SDK's internal caches for a market outcome.", @@ -958,7 +968,7 @@ "description": "Result" }, "exchangeOnly": "polymarket", - "source": "index.ts:201" + "source": "index.ts:209" }, "getEventById": { "summary": "Fetch a single event by its numeric ID (Probable only).", @@ -976,7 +986,7 @@ "description": "The UnifiedEvent, or null if not found" }, "exchangeOnly": "probable", - "source": "index.ts:141" + "source": "index.ts:151" }, "getEventBySlug": { "summary": "Fetch a single event by its URL slug (Probable only).", @@ -994,7 +1004,7 @@ "description": "The UnifiedEvent, or null if not found" }, "exchangeOnly": "probable", - "source": "index.ts:160" + "source": "index.ts:170" } }, "workflowExample": { diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index 37a4a598..dac681ce 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -297,6 +297,31 @@ exchange.fetch_events(query="Trump", limit=10, offset=0) **Notes:** Some exchanges (like Limitless) may only support status 'active' for search results. +--- +### `fetch_series` + +Fetch the recurring series (fourth tier above Event -> Market -> Outcome) + + +**Signature:** + +```python +def fetch_series(params: Optional[SeriesFetchParams] = None) -> List[UnifiedSeries]: +``` + +**Parameters:** + +- `params` (SeriesFetchParams) - **Optional**: params + +**Returns:** List[[UnifiedSeries](#unifiedseries)] - Array of unified series. Always an array, including the singular-lookup case. + +**Example:** + +```python +exchange.fetch_series() +``` + + --- ### `fetch_market` @@ -1443,6 +1468,7 @@ tags: List[string] # Optional list of tags. More granular than category — e.g. tick_size: float # Minimum price increment (e.g., 0.01, 0.001) status: str # Venue-native lifecycle status (e.g. 'active', 'closed', 'archived'). contract_address: str # On-chain contract / condition identifier where applicable (Polymarket conditionId, etc.). +source_metadata: object # Raw venue-specific metadata not captured by first-class fields (e.g. Kalshi series_ticker / series_title from the parent event, Polymarket series). Passed through verbatim so downstream consumers can recover anything the unified shape omits. Each venue populates what it has. source_exchange: str # The exchange/venue this market originates from (e.g. 'polymarket', 'kalshi'). Populated by the Router. yes: Any # Convenience accessor for the YES outcome on a binary market. no: Any # Convenience accessor for the NO outcome on a binary market. @@ -1485,9 +1511,31 @@ url: str # Canonical URL to view the event on the venue. image: str # Optional image URL for the event. category: str # Optional category label. Venue-defined — common values include "Sports", "Politics", "Crypto", "Economics", "Science", "Culture". Polymarket uses finer-grained categories like "Bitcoin", "Soccer", "Economic Policy"; Kalshi uses broader ones like "Sports" or "Mentions". tags: List[string] # Optional list of tags. More granular than category — e.g. ["Sports", "FIFA World Cup", "2026 FIFA World Cup"] or ["Politics", "Geopolitics", "Middle East"]. Tags vary by venue: Polymarket markets carry several, Kalshi typically one. +source_metadata: object # Raw venue-specific metadata not captured by first-class fields (e.g. Kalshi series_ticker / series_title, Polymarket series). Passed through verbatim so downstream consumers can recover anything the unified shape omits. Each venue populates what it has. source_exchange: str # The exchange/venue this event originates from (e.g. 'polymarket', 'kalshi'). Populated by the Router. ``` +--- +### `UnifiedSeries` + +A recurring grouping of events on a venue — the fourth tier above Event -> Market -> Outcome. Examples: Kalshi `KXATPMATCH` (every ATP tennis match), Polymarket `wta` (every WTA match), Opinion's daily `collection`. Series only exists where the venue exposes a recurring-event concept; venues without one return an empty array from `fetchSeries`. + +```python +@dataclass +class UnifiedSeries: +id: str # Stable venue-native series identifier (e.g. "KXATPMATCH" on Kalshi, "atp" on Polymarket Gamma, numeric Gamma id). +ticker: str # Venue-native ticker, when distinct from `id`. +slug: str # Venue-native slug. +title: str # Human-readable series title (e.g. "ATP Match Winner", "WTA"). +description: Any # Long-form series description. +recurrence: Any # Recurrence cadence the venue reports ('daily', 'weekly', 'annual', ...). +events: List[UnifiedEvent] # Child events. Populated when fetched by id; the list form usually omits this to keep payloads small. +url: Any # Canonical venue URL for the series. +image: Any # Venue-hosted image. +source_exchange: str # The exchange this series originates from. Populated by the Router. +source_metadata: object # Raw venue-specific fields not promoted to first-class columns. +``` + --- ### `PriceCandle` @@ -1934,6 +1982,7 @@ status: str # Filter by event status (default: 'active', 'inactive' and 'closed' search_in: str # Where to search (default: 'title') event_id: str # Direct lookup by event ID slug: str # Lookup by event slug +series: str # Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to `sourceMetadata` after fetch. filter: Any # Optional client-side filter applied after fetching category: str # Filter by category. Each event belongs to a venue-assigned category such as "Sports", "Politics", "Crypto", "Bitcoin", "Soccer", "Economic Policy" (Polymarket) or "Sports", "Mentions" (Kalshi). tags: List[string] # Filter by tags. Returns events matching ANY of the provided tags. Tags are more specific than categories -- for example a "Politics" event might carry tags ["Politics", "Geopolitics", "Middle East", "Iran"]. Common tags include "Crypto", "Elections", "Fed Rates", "FIFA World Cup", "Trump". diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index 44b6bce9..f8504e0a 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -300,6 +300,31 @@ await exchange.fetchEvents({ query: "Trump", limit: 10, offset: 0 }) **Notes:** Some exchanges (like Limitless) may only support status 'active' for search results. +--- +### `fetchSeries` + +Fetch the recurring series (fourth tier above Event -> Market -> Outcome) + + +**Signature:** + +```typescript +async fetchSeries(params?: SeriesFetchParams): Promise +``` + +**Parameters:** + +- `params` (SeriesFetchParams) - **Optional**: params + +**Returns:** Promise<[UnifiedSeries](#unifiedseries)[]> - Array of unified series. Always an array, including the singular-lookup case. + +**Example:** + +```typescript +await exchange.fetchSeries() +``` + + --- ### `fetchMarket` @@ -1443,6 +1468,7 @@ tags: string[]; // Optional list of tags. More granular than category — e.g. [ tickSize: number; // Minimum price increment (e.g., 0.01, 0.001) status: string; // Venue-native lifecycle status (e.g. 'active', 'closed', 'archived'). contractAddress: string; // On-chain contract / condition identifier where applicable (Polymarket conditionId, etc.). +sourceMetadata: object; // Raw venue-specific metadata not captured by first-class fields (e.g. Kalshi series_ticker / series_title from the parent event, Polymarket series). Passed through verbatim so downstream consumers can recover anything the unified shape omits. Each venue populates what it has. sourceExchange: string; // The exchange/venue this market originates from (e.g. 'polymarket', 'kalshi'). Populated by the Router. yes: any; // Convenience accessor for the YES outcome on a binary market. no: any; // Convenience accessor for the NO outcome on a binary market. @@ -1485,10 +1511,32 @@ url: string; // Canonical URL to view the event on the venue. image: string; // Optional image URL for the event. category: string; // Optional category label. Venue-defined — common values include "Sports", "Politics", "Crypto", "Economics", "Science", "Culture". Polymarket uses finer-grained categories like "Bitcoin", "Soccer", "Economic Policy"; Kalshi uses broader ones like "Sports" or "Mentions". tags: string[]; // Optional list of tags. More granular than category — e.g. ["Sports", "FIFA World Cup", "2026 FIFA World Cup"] or ["Politics", "Geopolitics", "Middle East"]. Tags vary by venue: Polymarket markets carry several, Kalshi typically one. +sourceMetadata: object; // Raw venue-specific metadata not captured by first-class fields (e.g. Kalshi series_ticker / series_title, Polymarket series). Passed through verbatim so downstream consumers can recover anything the unified shape omits. Each venue populates what it has. sourceExchange: string; // The exchange/venue this event originates from (e.g. 'polymarket', 'kalshi'). Populated by the Router. } ``` +--- +### `UnifiedSeries` + +A recurring grouping of events on a venue — the fourth tier above Event -> Market -> Outcome. Examples: Kalshi `KXATPMATCH` (every ATP tennis match), Polymarket `wta` (every WTA match), Opinion's daily `collection`. Series only exists where the venue exposes a recurring-event concept; venues without one return an empty array from `fetchSeries`. + +```typescript +interface UnifiedSeries { +id: string; // Stable venue-native series identifier (e.g. "KXATPMATCH" on Kalshi, "atp" on Polymarket Gamma, numeric Gamma id). +ticker: string; // Venue-native ticker, when distinct from `id`. +slug: string; // Venue-native slug. +title: string; // Human-readable series title (e.g. "ATP Match Winner", "WTA"). +description: any; // Long-form series description. +recurrence: any; // Recurrence cadence the venue reports ('daily', 'weekly', 'annual', ...). +events: UnifiedEvent[]; // Child events. Populated when fetched by id; the list form usually omits this to keep payloads small. +url: any; // Canonical venue URL for the series. +image: any; // Venue-hosted image. +sourceExchange: string; // The exchange this series originates from. Populated by the Router. +sourceMetadata: object; // Raw venue-specific fields not promoted to first-class columns. +} +``` + --- ### `PriceCandle` @@ -1934,6 +1982,7 @@ status?: string; // Filter by event status (default: 'active', 'inactive' and 'c searchIn?: string; // Where to search (default: 'title') eventId?: string; // Direct lookup by event ID slug?: string; // Lookup by event slug +series?: string; // Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to `sourceMetadata` after fetch. filter?: any; // Optional client-side filter applied after fetching category?: string; // Filter by category. Each event belongs to a venue-assigned category such as "Sports", "Politics", "Crypto", "Bitcoin", "Soccer", "Economic Policy" (Polymarket) or "Sports", "Mentions" (Kalshi). tags?: string[]; // Filter by tags. Returns events matching ANY of the provided tags. Tags are more specific than categories -- for example a "Politics" event might carry tags ["Politics", "Geopolitics", "Middle East", "Iran"]. Common tags include "Crypto", "Elections", "Fed Rates", "FIFA World Cup", "Trump". From db06e389efe2564f954d9508dbda4279b7cdc9ee Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Mon, 8 Jun 2026 09:03:12 +0000 Subject: [PATCH 3/3] docs: regenerate generated artifacts for PR #663 --- core/api-doc-config.generated.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index 134a795d..61d66822 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:31:38.004Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T09:03:09.995Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */",