From 0b32dbb55dbac82e58ff4bed663ede3b14d6c560 Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 23 Jun 2026 14:02:45 +0530 Subject: [PATCH 1/3] feat(exchanges/hunch): consume live list odds + 24h volume + align category/tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hunch's agent list item now carries live binary odds, a trailing-24h volume, and stays crypto-native in category. Wire all three into the normalizer so the hosted catalog surfaces Hunch properly: - Price binary YES/NO from `raw.odds` on the bare list path (explicit detail/quote odds still win) — Hunch markets no longer ingest at price 0, so they appear in price-gated compare/arbitrage/hedge/matched-prices. - `volume24h` now passes through `raw.volume24hUsd` (was hard-0) — the recency signal the catalog ranks on. - Map Hunch's 17 native categories onto pmxt's top-level taxonomy ("Crypto", "event"→"Culture") + granular tags (top category + subtype label + token), so Hunch answers `?category=` filters and matches with higher confidence. New raw fields are optional → the adapter is correct before AND after the Hunch API deploys. 54 normalizer tests (+7). tsc clean. Co-Authored-By: Claude Opus 4.8 --- core/src/exchanges/hunch/fetcher.ts | 4 + core/src/exchanges/hunch/utils.ts | 73 +++++++++++++++++-- .../test/normalizers/hunch-normalizer.test.ts | 47 +++++++++++- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/core/src/exchanges/hunch/fetcher.ts b/core/src/exchanges/hunch/fetcher.ts index b24cdb62..156c80b7 100644 --- a/core/src/exchanges/hunch/fetcher.ts +++ b/core/src/exchanges/hunch/fetcher.ts @@ -47,6 +47,10 @@ export interface HunchRawMarket { /** Present in the schema; absent on the bare list endpoint — optional. */ volumeUsd?: number; totalBets?: number; + /** Trailing-24h pool inflow (USD); absent on older API builds — optional. */ + volume24hUsd?: number; + /** Live binary YES/NO odds on the list item; null/absent for N-way markets. */ + odds?: HunchRawBinaryOdds | null; targetMarketCapUsd: number | null; outcomes: HunchRawOutcome[] | null; headline?: string | null; diff --git a/core/src/exchanges/hunch/utils.ts b/core/src/exchanges/hunch/utils.ts index c8c99600..9d68c37e 100644 --- a/core/src/exchanges/hunch/utils.ts +++ b/core/src/exchanges/hunch/utils.ts @@ -104,6 +104,62 @@ export function parseHunchSide(outcomeId: string): { marketId: string; side: str }; } +// --------------------------------------------------------------------------- +// Category + tags — map Hunch's fine-grained native taxonomy onto pmxt's +// top-level categories (Crypto / Culture / …) + granular tags, so Hunch +// markets answer `?category=` filters and match with higher confidence. +// --------------------------------------------------------------------------- + +/** Hunch native categories that are NOT crypto (manual-resolution markets). */ +const HUNCH_TOP_CATEGORY: Record = { + event: 'Culture', +}; + +/** + * Map a Hunch native category to a pmxt top-level category. Hunch is + * crypto-native, so every token/price/on-chain subtype rolls up to "Crypto"; + * only the manual "event" markets (fights/debates) map elsewhere. + */ +export function mapHunchCategory(rawCategory: string | undefined): string { + return HUNCH_TOP_CATEGORY[rawCategory ?? ''] ?? 'Crypto'; +} + +/** Human-readable granular label per Hunch native category (for tags). */ +const HUNCH_SUBTYPE_LABEL: Record = { + market_cap: 'Market Cap', + token_mcap_range: 'Market Cap', + token_mcap_flip: 'Market Cap', + token_mcap_close: 'Market Cap', + token_basket_mcap: 'Market Cap', + price_direction: 'Price', + token_price_range: 'Price', + token_return: 'Returns', + token_rank_milestone: 'Ranking', + chain_volume: 'On-chain Volume', + chain_throughput: 'Throughput', + chain_stablecoins: 'Stablecoins', + launchpad_volume: 'Launchpad', + dune_metric: 'On-chain', + volume_eta: 'Volume', + event: 'Event', +}; + +function titleCaseSlug(s: string): string { + return s + .split(/[_\s-]+/) + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +/** Tags = top category + a human subtype label + the token (deduped, non-empty). */ +export function hunchMarketTags(raw: HunchRawMarket): string[] { + const top = mapHunchCategory(raw.category); + const label = + HUNCH_SUBTYPE_LABEL[raw.category] ?? (raw.category ? titleCaseSlug(raw.category) : ''); + return [...new Set([top, label, raw.tokenSymbol].filter((t): t is string => Boolean(t)))]; +} + /** * Shared market normalizer used by both the live fetch path and the (rare) * direct-mapping helper. Pulls a Hunch market ref into a {@link UnifiedMarket}. @@ -125,6 +181,9 @@ export function mapHunchMarketToUnified( if (!raw || !raw.id) return null; const marketId = raw.id; + // Explicit odds (detail/quote read) win; else fall back to the live odds the + // list item now carries — so a bare list-crawl prices binary markets too. + const effectiveOdds = odds ?? raw.odds ?? null; let outcomes: MarketOutcome[]; if (Array.isArray(raw.outcomes) && raw.outcomes.length > 0) { @@ -154,10 +213,10 @@ export function mapHunchMarketToUnified( }); } else { // Binary YES/NO market. - const yesPrice = odds && typeof odds.yesPriceCents === 'number' ? odds.yesPriceCents / 100 : 0; + const yesPrice = effectiveOdds && typeof effectiveOdds.yesPriceCents === 'number' ? effectiveOdds.yesPriceCents / 100 : 0; const noPrice = - odds && typeof odds.noPriceCents === 'number' - ? odds.noPriceCents / 100 + effectiveOdds && typeof effectiveOdds.noPriceCents === 'number' + ? effectiveOdds.noPriceCents / 100 : yesPrice > 0 ? 1 - yesPrice : 0; @@ -174,13 +233,13 @@ export function mapHunchMarketToUnified( slug: raw.slug, outcomes, resolutionDate: raw.deadlineAt ? new Date(raw.deadlineAt) : undefined, - // Hunch is parimutuel and reports no 24h volume split — surface 0. - volume24h: 0, + // Trailing-24h pool inflow off the list item (0 when absent / no trades). + volume24h: typeof raw.volume24hUsd === 'number' ? raw.volume24hUsd : 0, volume: typeof raw.volumeUsd === 'number' ? raw.volumeUsd : undefined, liquidity: Number(raw.virtualLiquidityUsd || 0), url: raw.links?.app || `${DEFAULT_BASE_URL}/markets/${raw.slug || marketId}`, - category: raw.category, - tags: raw.tokenSymbol ? [raw.tokenSymbol] : [], + category: mapHunchCategory(raw.category), + tags: hunchMarketTags(raw), status: mapHunchStatus(raw.status), sourceMetadata: buildSourceMetadata( raw as unknown as Record, diff --git a/core/test/normalizers/hunch-normalizer.test.ts b/core/test/normalizers/hunch-normalizer.test.ts index aa38bb6a..f539da58 100644 --- a/core/test/normalizers/hunch-normalizer.test.ts +++ b/core/test/normalizers/hunch-normalizer.test.ts @@ -151,7 +151,7 @@ describe('HunchNormalizer', () => { expect(market().volume).toBe(1_240); }); - it('volume24h is 0 (Hunch reports no 24h split)', () => { + it('volume24h is 0 when this fixture carries no volume24hUsd', () => { expect(market().volume24h).toBe(0); }); @@ -407,4 +407,49 @@ describe('HunchNormalizer', () => { expect(normalizer.normalizeBalance(readiness)[0].total).toBe(0); }); }); + + // ------------------------------------------------------------------------- + // pmxt surfacing: live odds + 24h volume + category/tags off the LIST item + // ------------------------------------------------------------------------- + describe('normalizeMarket — list odds + 24h volume (pmxt surfacing)', () => { + it('prices binary YES/NO from raw.odds on the bare list path (no explicit odds arg)', () => { + const raw = { ...binaryMarket, odds: { yesPriceCents: 64, noPriceCents: 36 } } as HunchRawMarket; + const m = normalizer.normalizeMarket(raw)!; + expect(m.outcomes[0].price).toBeCloseTo(0.64, 5); + expect(m.outcomes[1].price).toBeCloseTo(0.36, 5); + }); + + it('lets an explicit odds arg (detail/quote) win over raw.odds', () => { + const raw = { ...binaryMarket, odds: { yesPriceCents: 64, noPriceCents: 36 } } as HunchRawMarket; + const m = normalizer.normalizeMarket(raw, { yesPriceCents: 90, noPriceCents: 10 })!; + expect(m.outcomes[0].price).toBeCloseTo(0.9, 5); + }); + + it('passes through volume24h from raw.volume24hUsd', () => { + const raw = { ...binaryMarket, volume24hUsd: 320 } as HunchRawMarket; + expect(normalizer.normalizeMarket(raw)!.volume24h).toBe(320); + }); + + it('volume24h is 0 when the list item carries no 24h figure', () => { + expect(normalizer.normalizeMarket(binaryMarket)!.volume24h).toBe(0); + }); + }); + + describe('category + tags alignment (pmxt taxonomy)', () => { + it('maps a Hunch crypto subtype to the top-level "Crypto" category', () => { + expect(normalizer.normalizeMarket(binaryMarket)!.category).toBe('Crypto'); + }); + + it('maps an event market to "Culture"', () => { + const raw = { ...binaryMarket, category: 'event' } as HunchRawMarket; + expect(normalizer.normalizeMarket(raw)!.category).toBe('Culture'); + }); + + it('tags carry the top category, a human subtype label, and the token', () => { + const tags = normalizer.normalizeMarket(binaryMarket)!.tags ?? []; + expect(tags).toContain('Crypto'); + expect(tags).toContain('Market Cap'); + expect(tags).toContain('HUNCH'); + }); + }); }); From 4fea30411ffa25c62d491a8ba58d379a6e5ca61c Mon Sep 17 00:00:00 2001 From: Raj Date: Tue, 23 Jun 2026 14:19:08 +0530 Subject: [PATCH 2/3] feat(exchanges/hunch): drain the market list via nextCursor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hunch list now paginates. A catalog crawl (no explicit limit) follows `nextCursor` to drain the whole catalogue — so the hosted catalog ingests every Hunch market, not just the first 200. An explicit limit still fetches one page. MAX_LIST_PAGES (50) backstops a runaway cursor loop. Co-Authored-By: Claude Opus 4.8 --- core/src/exchanges/hunch/fetcher.ts | 25 +++++++++---- core/test/exchanges/hunch-fetcher.test.ts | 44 +++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 core/test/exchanges/hunch-fetcher.test.ts diff --git a/core/src/exchanges/hunch/fetcher.ts b/core/src/exchanges/hunch/fetcher.ts index 156c80b7..a06f1c6f 100644 --- a/core/src/exchanges/hunch/fetcher.ts +++ b/core/src/exchanges/hunch/fetcher.ts @@ -5,6 +5,8 @@ import { hunchErrorMapper } from './errors'; const AGENT_PREFIX = '/api/agent/v1'; const DEFAULT_LIMIT = 200; +/** Safety backstop for cursor draining: 50 pages * 200 = 10k markets. */ +const MAX_LIST_PAGES = 50; // --------------------------------------------------------------------------- // Raw venue-native shapes (what the Hunch agent API returns). Mirror the Zod @@ -208,6 +210,7 @@ export class HunchFetcher return one ? [one] : []; } + const explicitLimit = typeof params?.limit === 'number'; const query: Record = { limit: params?.limit ?? DEFAULT_LIMIT, }; @@ -215,12 +218,22 @@ export class HunchFetcher if (status) query.status = status; if (params?.query) query.token = params.query; - const res = await this.ctx.http.get(`${this.baseUrl}${AGENT_PREFIX}/markets`, { - params: query, - headers: this.ctx.getHeaders(), - }); - const markets: HunchRawMarket[] = res.data?.markets ?? []; - return markets; + // A catalog crawl (no explicit limit) follows `nextCursor` to drain + // the whole list; an explicit limit fetches just that one page. + // MAX_LIST_PAGES is a safety backstop against a runaway cursor loop. + const all: HunchRawMarket[] = []; + let cursor: string | undefined; + let pages = 0; + do { + const res = await this.ctx.http.get(`${this.baseUrl}${AGENT_PREFIX}/markets`, { + params: cursor ? { ...query, cursor } : query, + headers: this.ctx.getHeaders(), + }); + all.push(...((res.data?.markets ?? []) as HunchRawMarket[])); + cursor = typeof res.data?.nextCursor === 'string' ? res.data.nextCursor : undefined; + pages += 1; + } while (!explicitLimit && cursor && pages < MAX_LIST_PAGES); + return all; } catch (error: unknown) { throw hunchErrorMapper.mapError(error); } diff --git a/core/test/exchanges/hunch-fetcher.test.ts b/core/test/exchanges/hunch-fetcher.test.ts new file mode 100644 index 00000000..b77766c6 --- /dev/null +++ b/core/test/exchanges/hunch-fetcher.test.ts @@ -0,0 +1,44 @@ +import { HunchFetcher } from '../../src/exchanges/hunch/fetcher'; +import { FetcherContext } from '../../src/exchanges/interfaces'; + +// Sprint 6 (pmxt surfacing): the Hunch list now paginates via nextCursor. A +// catalog crawl (no explicit limit) must follow the cursor to drain the WHOLE +// catalogue — else it silently truncates at the first page once Hunch grows. + +function fetcherWithPages(pages: Array<{ markets: unknown[]; nextCursor: string | null }>) { + let calls = 0; + const ctx = { + http: { + get: async () => { + const page = pages[calls] ?? { markets: [], nextCursor: null }; + calls += 1; + return { data: page }; + }, + } as unknown as FetcherContext['http'], + callApi: async () => ({}), + getHeaders: () => ({}), + } as FetcherContext; + return { fetcher: new HunchFetcher(ctx), getCalls: () => calls }; +} + +describe('HunchFetcher.fetchRawMarkets — cursor draining', () => { + it('drains every page via nextCursor when no explicit limit is set', async () => { + const { fetcher, getCalls } = fetcherWithPages([ + { markets: [{ id: 'a' }, { id: 'b' }], nextCursor: 'cur-1' }, + { markets: [{ id: 'c' }], nextCursor: null }, + ]); + const all = await fetcher.fetchRawMarkets(); + expect(all.map((m) => m.id)).toEqual(['a', 'b', 'c']); + expect(getCalls()).toBe(2); + }); + + it('fetches only a single page when an explicit limit is set', async () => { + const { fetcher, getCalls } = fetcherWithPages([ + { markets: [{ id: 'a' }], nextCursor: 'cur-1' }, + { markets: [{ id: 'b' }], nextCursor: null }, + ]); + const all = await fetcher.fetchRawMarkets({ limit: 1 }); + expect(all.map((m) => m.id)).toEqual(['a']); + expect(getCalls()).toBe(1); + }); +}); From ec8a30845ee008476a959c08bda77efffe3ccd52 Mon Sep 17 00:00:00 2001 From: Samuel Tinnerholm Date: Tue, 23 Jun 2026 09:49:18 +0000 Subject: [PATCH 3/3] chore(hunch): sync consumer SDK surfaces --- core/api-doc-config.generated.json | 86 +++++++++---------- sdks/python/API_REFERENCE.md | 6 ++ sdks/python/pmxt/__init__.py | 3 +- sdks/python/pmxt/_exchanges.py | 28 ++++++ .../python/scripts/generate-client-methods.js | 61 ++++++++++--- sdks/typescript/API_REFERENCE.md | 7 +- sdks/typescript/index.ts | 5 +- sdks/typescript/pmxt/client.ts | 13 +++ .../scripts/generate-client-methods.js | 56 ++++++++++-- 9 files changed, 199 insertions(+), 66 deletions(-) diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index a97b1945..dcb8d43b 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,9 +1,9 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T10:51:09.860Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-23T09:48:25.995Z. 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 /** 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`.", + "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 /** Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. */\n sourceExchange?: string;\n /** Alias for `sourceExchange`. */\n exchange?: string;\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 /** Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. */\n sourceExchange?: string;\n /** Alias for `sourceExchange`. */\n exchange?: 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", @@ -19,7 +19,7 @@ "type": "ImplicitApiMethodInfo[]", "description": "Result" }, - "source": "BaseExchange.ts:451" + "source": "BaseExchange.ts:459" }, "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:564" + "source": "BaseExchange.ts:572" }, "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:601" + "source": "BaseExchange.ts:609" }, "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:650" + "source": "BaseExchange.ts:658" }, "fetchEventsPaginated": { "summary": "Paginated variant of {@link fetchEvents}.", @@ -137,7 +137,7 @@ "type": "PaginatedEventsResult", "description": "PaginatedEventsResult with data, total, and optional nextCursor" }, - "source": "BaseExchange.ts:719" + "source": "BaseExchange.ts:727" }, "fetchEvents": { "summary": "Fetch events with optional keyword search.", @@ -175,7 +175,7 @@ "notes": [ "Some exchanges (like Limitless) may only support status 'active' for search results." ], - "source": "BaseExchange.ts:788" + "source": "BaseExchange.ts:796" }, "fetchSeries": { "summary": "Fetch the recurring series (fourth tier above Event -> Market -> Outcome)", @@ -192,7 +192,7 @@ "type": "UnifiedSeries[]", "description": "Array of unified series. Always an array, including the singular-lookup case." }, - "source": "BaseExchange.ts:823" + "source": "BaseExchange.ts:831" }, "fetchMarket": { "summary": "Fetch a single market by lookup parameters.", @@ -209,7 +209,7 @@ "type": "UnifiedMarket", "description": "A single unified market" }, - "source": "BaseExchange.ts:841" + "source": "BaseExchange.ts:849" }, "fetchEvent": { "summary": "Fetch a single event by lookup parameters.", @@ -226,7 +226,7 @@ "type": "UnifiedEvent", "description": "A single unified event" }, - "source": "BaseExchange.ts:941" + "source": "BaseExchange.ts:949" }, "fetchOHLCV": { "summary": "Fetch historical OHLCV (candlestick) price data for a specific market outcome.", @@ -254,7 +254,7 @@ "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:958" + "source": "BaseExchange.ts:966" }, "fetchOrderBook": { "summary": "Fetch the order book (bids/asks) for a specific outcome.", @@ -283,7 +283,7 @@ "type": "OrderBook", "description": "Order book with bids and asks. Returns OrderBook[] when" }, - "source": "BaseExchange.ts:973" + "source": "BaseExchange.ts:981" }, "fetchOrderBooks": { "summary": "Batch variant of {@link fetchOrderBook}. Fetches order books for", @@ -300,7 +300,7 @@ "type": "Record", "description": "A map keyed by the input id (preserving the caller's exact" }, - "source": "BaseExchange.ts:1001" + "source": "BaseExchange.ts:1009" }, "fetchTrades": { "summary": "Fetch raw trade history for a specific outcome.", @@ -326,7 +326,7 @@ "notes": [ "Polymarket requires an API key for trade history. Use fetchOHLCV for public historical data." ], - "source": "BaseExchange.ts:1014" + "source": "BaseExchange.ts:1022" }, "createOrder": { "summary": "Place a new order on the exchange.", @@ -343,7 +343,7 @@ "type": "Order", "description": "The created order" }, - "source": "BaseExchange.ts:1031" + "source": "BaseExchange.ts:1039" }, "buildOrder": { "summary": "Build an order payload without submitting it to the exchange.", @@ -360,7 +360,7 @@ "type": "BuiltOrder", "description": "A BuiltOrder containing the exchange-native payload" }, - "source": "BaseExchange.ts:1045" + "source": "BaseExchange.ts:1053" }, "submitOrder": { "summary": "Submit a pre-built order returned by buildOrder().", @@ -377,7 +377,7 @@ "type": "Order", "description": "The submitted order" }, - "source": "BaseExchange.ts:1057" + "source": "BaseExchange.ts:1065" }, "cancelOrder": { "summary": "Cancel an existing open order.", @@ -394,7 +394,7 @@ "type": "Order", "description": "The cancelled order" }, - "source": "BaseExchange.ts:1067" + "source": "BaseExchange.ts:1075" }, "fetchOrder": { "summary": "Fetch a specific order by ID.", @@ -411,7 +411,7 @@ "type": "Order", "description": "The order details" }, - "source": "BaseExchange.ts:1077" + "source": "BaseExchange.ts:1085" }, "fetchOpenOrders": { "summary": "Fetch all open orders, optionally filtered by market.", @@ -428,7 +428,7 @@ "type": "Order[]", "description": "Array of open orders" }, - "source": "BaseExchange.ts:1087" + "source": "BaseExchange.ts:1095" }, "fetchPositions": { "summary": "Fetch current user positions across all markets.", @@ -445,7 +445,7 @@ "type": "Position[]", "description": "Array of user positions" }, - "source": "BaseExchange.ts:1109" + "source": "BaseExchange.ts:1117" }, "fetchBalance": { "summary": "Fetch account balances.", @@ -462,7 +462,7 @@ "type": "Balance[]", "description": "Array of account balances" }, - "source": "BaseExchange.ts:1119" + "source": "BaseExchange.ts:1127" }, "getExecutionPrice": { "summary": "Calculate the volume-weighted average execution price for a given order size.", @@ -491,7 +491,7 @@ "type": "number", "description": "Average execution price, or 0 if insufficient liquidity" }, - "source": "BaseExchange.ts:1129" + "source": "BaseExchange.ts:1137" }, "getExecutionPriceDetailed": { "summary": "Calculate detailed execution price information including partial fill data.", @@ -520,7 +520,7 @@ "type": "ExecutionPriceResult", "description": "Detailed execution result with price, filled amount, and fill status" }, - "source": "BaseExchange.ts:1142" + "source": "BaseExchange.ts:1150" }, "filterMarkets": { "summary": "Filter a list of markets by criteria.", @@ -543,7 +543,7 @@ "type": "UnifiedMarket[]", "description": "Filtered array of markets" }, - "source": "BaseExchange.ts:1158" + "source": "BaseExchange.ts:1166" }, "filterEvents": { "summary": "Filter a list of events by criteria.", @@ -566,7 +566,7 @@ "type": "UnifiedEvent[]", "description": "Filtered array of events" }, - "source": "BaseExchange.ts:1318" + "source": "BaseExchange.ts:1326" }, "watchOrderBook": { "summary": "Watch order book updates in real-time via WebSocket.", @@ -595,7 +595,7 @@ "type": "OrderBook", "description": "Promise that resolves with the current orderbook state" }, - "source": "BaseExchange.ts:1414" + "source": "BaseExchange.ts:1422" }, "watchOrderBooks": { "summary": "Watch multiple order books simultaneously via WebSocket.", @@ -624,7 +624,7 @@ "type": "Record", "description": "Promise that resolves with order books keyed by ID" }, - "source": "BaseExchange.ts:1427" + "source": "BaseExchange.ts:1435" }, "unwatchOrderBook": { "summary": "Unsubscribe from a previously watched order book stream.", @@ -641,7 +641,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1455" + "source": "BaseExchange.ts:1463" }, "watchTrades": { "summary": "Watch trade executions in real-time via WebSocket.", @@ -676,7 +676,7 @@ "type": "Trade[]", "description": "Promise that resolves with recent trades" }, - "source": "BaseExchange.ts:1468" + "source": "BaseExchange.ts:1476" }, "watchAddress": { "summary": "Stream activity for a public wallet address", @@ -699,7 +699,7 @@ "type": "SubscribedAddressSnapshot", "description": "Promise that resolves with the latest SubscribedAddressSnapshot snapshot" }, - "source": "BaseExchange.ts:1482" + "source": "BaseExchange.ts:1490" }, "unwatchAddress": { "summary": "Stop watching a previously registered wallet address and release its resource updates.", @@ -716,7 +716,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1495" + "source": "BaseExchange.ts:1503" }, "close": { "summary": "Close all WebSocket connections and clean up resources.", @@ -726,7 +726,7 @@ "type": "void", "description": "Result" }, - "source": "BaseExchange.ts:1504" + "source": "BaseExchange.ts:1512" }, "fetchMarketMatches": { "summary": "Find the same or related market on other venues. Two modes:", @@ -743,7 +743,7 @@ "type": "MatchResult[]", "description": "Array of matched markets with relation and confidence" }, - "source": "BaseExchange.ts:1518" + "source": "BaseExchange.ts:1526" }, "fetchMatches": { "summary": "fetchMatches", @@ -760,7 +760,7 @@ "type": "MatchResult[]", "description": "Result" }, - "source": "BaseExchange.ts:1534" + "source": "BaseExchange.ts:1542" }, "fetchEventMatches": { "summary": "Find the same or related event on other venues. Two modes:", @@ -777,7 +777,7 @@ "type": "EventMatchResult[]", "description": "Array of matched events with market-level match details" }, - "source": "BaseExchange.ts:1542" + "source": "BaseExchange.ts:1550" }, "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.", @@ -794,7 +794,7 @@ "type": "PriceComparison[]", "description": "Array of price comparisons across venues" }, - "source": "BaseExchange.ts:1558" + "source": "BaseExchange.ts:1566" }, "fetchRelatedMarkets": { "summary": "Find related markets across venues. Discovers subset/superset market relationships", @@ -811,7 +811,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1568" + "source": "BaseExchange.ts:1576" }, "fetchMatchedMarkets": { "summary": "fetchMatchedMarkets", @@ -828,7 +828,7 @@ "type": "MatchedMarketPair[]", "description": "Result" }, - "source": "BaseExchange.ts:1579" + "source": "BaseExchange.ts:1587" }, "fetchMatchedPrices": { "summary": "fetchMatchedPrices", @@ -845,7 +845,7 @@ "type": "MatchedPricePair[]", "description": "Array of matched market pairs with prices from each venue" }, - "source": "BaseExchange.ts:1587" + "source": "BaseExchange.ts:1595" }, "fetchHedges": { "summary": "fetchHedges", @@ -862,7 +862,7 @@ "type": "PriceComparison[]", "description": "Array of subset/superset matches with live prices" }, - "source": "BaseExchange.ts:1598" + "source": "BaseExchange.ts:1606" }, "fetchArbitrage": { "summary": "fetchArbitrage", @@ -879,7 +879,7 @@ "type": "ArbitrageOpportunity[]", "description": "Array of arbitrage opportunities sorted by spread" }, - "source": "BaseExchange.ts:1608" + "source": "BaseExchange.ts:1616" }, "watchPrices": { "summary": "Watch AMM price updates for a market address (Limitless only).", diff --git a/sdks/python/API_REFERENCE.md b/sdks/python/API_REFERENCE.md index ae37ce21..0acaca0a 100644 --- a/sdks/python/API_REFERENCE.md +++ b/sdks/python/API_REFERENCE.md @@ -1632,6 +1632,8 @@ amount: float # Size of the trade in contracts/shares. side: str # Trade side from the taker's perspective. outcome_id: str # The outcome this trade is for (if known). order_id: str # The order that produced this trade, if known. +market_id: str # The market this trade belongs to, when the venue exposes it (e.g. derivable from the fill's coin/asset). +fee: float # Trading fee paid by the user for this fill, when the venue exposes it. tx_hash: str # Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. chain: str # Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. block_number: float # Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. @@ -1998,6 +2000,8 @@ outcome_id: str # Reverse lookup -- find market containing this outcome event_id: str # Find markets belonging to an event page: float # For pagination (used by Limitless) similarity_threshold: float # For semantic search (used by Limitless) +source_exchange: str # Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. +exchange: str # Alias for `sourceExchange`. ``` --- @@ -2021,6 +2025,8 @@ series: str # Filter events by their parent series. Accepts the venue-native ser 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". +source_exchange: str # Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. +exchange: str # Alias for `sourceExchange`. ``` --- diff --git a/sdks/python/pmxt/__init__.py b/sdks/python/pmxt/__init__.py index 4250bb2e..59fa1f96 100644 --- a/sdks/python/pmxt/__init__.py +++ b/sdks/python/pmxt/__init__.py @@ -20,7 +20,7 @@ from .client import Exchange from .constants import ENV, ENV_BASE_URL, ENV_API_KEY -from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, PolymarketUS, Polymarket_us, Hyperliquid, GeminiTitan, SuiBets, Suibets, Rain, Mock, Router +from ._exchanges import Polymarket, Limitless, Kalshi, KalshiDemo, Probable, Baozi, Myriad, Opinion, Metaculus, Smarkets, PolymarketUS, Polymarket_us, Hyperliquid, GeminiTitan, SuiBets, Suibets, Rain, Hunch, Mock, Router from .router import Router from .feed_client import FeedClient from .server_manager import ServerManager @@ -185,6 +185,7 @@ def restart_server() -> None: "SuiBets", "Suibets", "Rain", + "Hunch", "Mock", "Router", "Exchange", diff --git a/sdks/python/pmxt/_exchanges.py b/sdks/python/pmxt/_exchanges.py index b6402ad9..10bf30d6 100644 --- a/sdks/python/pmxt/_exchanges.py +++ b/sdks/python/pmxt/_exchanges.py @@ -542,6 +542,34 @@ def __init__( ) +class Hunch(Exchange): + """Hunch exchange client.""" + + def __init__( + self, + private_key: Optional[str] = None, + base_url: Optional[str] = None, + auto_start_server: Optional[bool] = None, + pmxt_api_key: Optional[str] = None, + ) -> None: + """ + Initialize Hunch client. + + Args: + private_key: Private key for authentication (optional) + base_url: Base URL of the PMXT sidecar server + auto_start_server: Automatically start server if not running (default: True) + pmxt_api_key: Hosted PMXT API key (optional; enables hosted mode) + """ + super().__init__( + exchange_name="hunch", + private_key=private_key, + base_url=base_url, + auto_start_server=auto_start_server, + pmxt_api_key=pmxt_api_key, + ) + + class Mock(Exchange): """Mock exchange client.""" diff --git a/sdks/python/scripts/generate-client-methods.js b/sdks/python/scripts/generate-client-methods.js index d944555a..ad0d4bb7 100644 --- a/sdks/python/scripts/generate-client-methods.js +++ b/sdks/python/scripts/generate-client-methods.js @@ -25,6 +25,31 @@ const CLIENT_PATH = path.join(__dirname, '../pmxt/client.py'); const MARKER_BEGIN = ' # BEGIN GENERATED METHODS'; const MARKER_END = ' # END GENERATED METHODS'; +// Methods with bespoke SDK/hosted behavior that still live inside the generated +// region. Preserve the checked-in method bodies while generator support catches up +// so codegen checks do not erase hand-maintained hosted routing shims. +const PRESERVE_EXISTING_METHODS = new Set([ + 'fetchEventsPaginated', + 'fetchMarket', + 'cancelOrder', + 'fetchOrder', + 'fetchOrderBook', + 'fetchOpenOrders', + 'fetchMyTrades', + 'fetchClosedOrders', + 'fetchAllOrders', + 'fetchPositions', + 'fetchBalance', + 'unwatchOrderBook', + 'fetchMatchedMarkets', +]); + +function extractExistingPyMethod(generatedRegion, snakeName) { + const re = new RegExp(`^ def ${snakeName}\\([^\\n]*\\)[^\\n]*:[\\s\\S]*?(?=^ def |^ # END GENERATED METHODS)`, 'm'); + const match = generatedRegion.match(re); + return match ? match[0].replace(/\n+$/, '') : null; +} + // Methods kept hand-maintained in client.py (special logic, streaming, local-only) const SKIP_GENERATE = new Set([ 'callApi', @@ -69,7 +94,8 @@ const TYPE_MAP = { OrderBook: { pyType: 'OrderBook', converter: '_convert_order_book' }, PriceCandle: { pyType: 'PriceCandle', converter: '_convert_candle' }, // Pagination wrapper: detected by name, not structure — gets its own response handler - PaginatedMarketsResult: { pyType: 'PaginatedMarketsResult', converter: null, pattern: 'paginated' }, + PaginatedMarketsResult: { pyType: 'PaginatedMarketsResult', converter: null, pattern: 'paginatedMarkets' }, + PaginatedEventsResult: { pyType: 'PaginatedEventsResult', converter: null, pattern: 'paginatedEvents' }, }; // Parameter names that represent outcome IDs and should accept MarketOutcome. @@ -187,8 +213,8 @@ function resolveReturnType(node, sf) { function inferReturnConfig(returnTypeNode, methodName, sf) { const resolved = resolveReturnType(returnTypeNode, sf); - if (resolved.pattern === 'paginated') { - return { returnPy: resolved.pyType, pattern: 'paginated', converter: null }; + if (resolved.pattern === 'paginatedMarkets' || resolved.pattern === 'paginatedEvents') { + return { returnPy: resolved.pyType, pattern: resolved.pattern, converter: null }; } if (resolved.pattern === 'void') { @@ -373,7 +399,7 @@ function buildPyReturnLines(config) { `${i}data = self._handle_response(json.loads(response.data))\n` + `${i}return {key: ${converter}(value) for key, value in (data or {}).items()}` ); - case 'paginated': + case 'paginatedMarkets': return [ `${i}data = self._handle_response(json.loads(response.data))`, `${i}return PaginatedMarketsResult(`, @@ -382,6 +408,15 @@ function buildPyReturnLines(config) { `${i} next_cursor=data.get("nextCursor"),`, `${i})`, ].join('\n'); + case 'paginatedEvents': + return [ + `${i}data = self._handle_response(json.loads(response.data))`, + `${i}return PaginatedEventsResult(`, + `${i} data=[_convert_event(e) for e in data.get("data", [])],`, + `${i} total=data.get("total"),`, + `${i} next_cursor=data.get("nextCursor"),`, + `${i})`, + ].join('\n'); case 'void': return `${i}self._handle_response(json.loads(response.data))`; default: @@ -485,12 +520,6 @@ function main() { const methods = extractMethods(sf); - const generated = methods.map(m => { - const name = m.name.text; - const config = inferReturnConfig(m.type, name, sf); - return generatePyMethod(name, m.parameters, config, sf); - }).join('\n\n'); - let client = fs.readFileSync(CLIENT_PATH, 'utf-8'); const beginIdx = client.indexOf(MARKER_BEGIN); @@ -504,8 +533,20 @@ function main() { } const before = client.slice(0, beginIdx + MARKER_BEGIN.length); + const existingRegion = client.slice(beginIdx + MARKER_BEGIN.length, endIdx); const after = client.slice(endIdx); + const generated = methods.map(m => { + const name = m.name.text; + const snakeName = camelToSnake(name); + if (PRESERVE_EXISTING_METHODS.has(name)) { + const existing = extractExistingPyMethod(existingRegion, snakeName); + if (existing) return existing; + } + const config = inferReturnConfig(m.type, name, sf); + return generatePyMethod(name, m.parameters, config, sf); + }).join('\n\n'); + client = `${before}\n\n${generated}\n\n${after}`; fs.writeFileSync(CLIENT_PATH, client, 'utf-8'); diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index 42ce6432..230cdb55 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -1493,7 +1493,6 @@ yes: any; // Convenience accessor for the YES outcome on a binary market. no: any; // Convenience accessor for the NO outcome on a binary market. up: any; // Convenience accessor for the UP outcome on a binary market. down: any; // Convenience accessor for the DOWN outcome on a binary market. -question: string; // Read-only alias for title. Matches the Python SDK's market.question property. } ``` @@ -1633,6 +1632,8 @@ amount: number; // Size of the trade in contracts/shares. side: string; // Trade side from the taker's perspective. outcomeId: string; // The outcome this trade is for (if known). orderId: string; // The order that produced this trade, if known. +marketId: string; // The market this trade belongs to, when the venue exposes it (e.g. derivable from the fill's coin/asset). +fee: number; // Trading fee paid by the user for this fill, when the venue exposes it. txHash: string; // Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. chain: string; // Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. blockNumber: number; // Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. @@ -1999,6 +2000,8 @@ outcomeId?: string; // Reverse lookup -- find market containing this outcome eventId?: string; // Find markets belonging to an event page?: number; // For pagination (used by Limitless) similarityThreshold?: number; // For semantic search (used by Limitless) +sourceExchange?: string; // Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. +exchange?: string; // Alias for `sourceExchange`. } ``` @@ -2022,6 +2025,8 @@ series?: string; // Filter events by their parent series. Accepts the venue-nati 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". +sourceExchange?: string; // Filter by source venue (e.g. 'polymarket', 'kalshi', 'myriad'). `exchange` is an alias. +exchange?: string; // Alias for `sourceExchange`. } ``` diff --git a/sdks/typescript/index.ts b/sdks/typescript/index.ts index b2858aca..a2afc425 100644 --- a/sdks/typescript/index.ts +++ b/sdks/typescript/index.ts @@ -19,14 +19,14 @@ */ -import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock } from "./pmxt/client.js"; +import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock } from "./pmxt/client.js"; import { Router } from "./pmxt/router.js"; import { ServerManager } from "./pmxt/server-manager.js"; import { FeedClient } from "./pmxt/feed-client.js"; import * as models from "./pmxt/models.js"; import * as errors from "./pmxt/errors.js"; -export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock, PolymarketOptions } from "./pmxt/client.js"; +export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock, PolymarketOptions } from "./pmxt/client.js"; export { FeedClient } from "./pmxt/feed-client.js"; export type { Ticker, Tickers, OHLCV, Market as FeedMarket, OracleRound, FeedClientOptions } from "./pmxt/feed-client.js"; export { Router } from "./pmxt/router.js"; @@ -98,6 +98,7 @@ const pmxt = { SuiBets, Suibets, Rain, + Hunch, Mock, Router, ServerManager, diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index b383d003..68a0bc6e 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -3419,6 +3419,19 @@ export class Rain extends Exchange { } } + +/** + * Hunch exchange client. + * + * Hunch is a crypto-native prediction market. Reads are unauthenticated; + * trading requires an EVM private key. + */ +export class Hunch extends Exchange { + constructor(options: ExchangeOptions = {}) { + super("hunch", options); + } +} + /** * Mock exchange client. * diff --git a/sdks/typescript/scripts/generate-client-methods.js b/sdks/typescript/scripts/generate-client-methods.js index 064743ef..d32faf2b 100644 --- a/sdks/typescript/scripts/generate-client-methods.js +++ b/sdks/typescript/scripts/generate-client-methods.js @@ -25,6 +25,29 @@ const CLIENT_PATH = path.join(__dirname, '../pmxt/client.ts'); const MARKER_BEGIN = ' // BEGIN GENERATED METHODS'; const MARKER_END = ' // END GENERATED METHODS'; +// Methods with bespoke SDK/hosted behavior that still live inside the generated +// region. Preserve the checked-in method bodies while generator support catches up +// so codegen checks do not erase hand-maintained hosted routing shims. +const PRESERVE_EXISTING_METHODS = new Set([ + 'fetchEventsPaginated', + 'cancelOrder', + 'fetchOrder', + 'fetchOrderBook', + 'fetchOpenOrders', + 'fetchMyTrades', + 'fetchClosedOrders', + 'fetchAllOrders', + 'fetchPositions', + 'fetchBalance', + 'fetchMatchedMarkets', +]); + +function extractExistingTsMethod(generatedRegion, methodName) { + const re = new RegExp(`^ async ${methodName}\\([^\\n]*\\)[^{]*\\{[\\s\\S]*?(?=^ async |^ // END GENERATED METHODS)`, 'm'); + const match = generatedRegion.match(re); + return match ? match[0].replace(/\n+$/, '') : null; +} + // Methods kept hand-maintained in client.ts (special logic, streaming, local-only) const SKIP_GENERATE = new Set([ 'callApi', @@ -64,12 +87,13 @@ const TYPE_MAP = { PriceCandle: { converter: 'convertCandle' }, // Pagination wrapper — gets its own response handler PaginatedMarketsResult: { converter: null, pattern: 'paginatedMarkets' }, + PaginatedEventsResult: { converter: null, pattern: 'paginatedEvents' }, }; // SDK types that can appear in generated signatures without extra imports const SDK_PARAM_TYPES = new Set([ 'UnifiedMarket', 'UnifiedEvent', 'UnifiedSeries', 'OrderBook', 'Order', 'Trade', - 'UserTrade', 'Position', 'Balance', 'PriceCandle', 'PaginatedMarketsResult', + 'UserTrade', 'Position', 'Balance', 'PriceCandle', 'PaginatedMarketsResult', 'PaginatedEventsResult', 'BuiltOrder', // Parameter types 'MarketFilterParams', 'MarketFetchParams', 'EventFetchParams', 'SeriesFetchParams', @@ -186,8 +210,8 @@ function resolveReturnType(node, sf) { function inferReturnConfig(returnTypeNode, methodName, sf) { const resolved = resolveReturnType(returnTypeNode, sf); - if (resolved.pattern === 'paginatedMarkets') { - return { returnTs: 'PaginatedMarketsResult', pattern: 'paginatedMarkets', converter: null }; + if (resolved.pattern === 'paginatedMarkets' || resolved.pattern === 'paginatedEvents') { + return { returnTs: resolved.returnTs, pattern: resolved.pattern, converter: null }; } if (resolved.pattern === 'void') { @@ -374,6 +398,15 @@ function buildReturnLines(config) { `${i} nextCursor: data.nextCursor,`, `${i}};`, ].join('\n'); + case 'paginatedEvents': + return [ + `${i}const data = this.handleResponse(json);`, + `${i}return {`, + `${i} data: (data.data || []).map(convertEvent),`, + `${i} total: data.total,`, + `${i} nextCursor: data.nextCursor,`, + `${i}};`, + ].join('\n'); case 'void': return `${i}this.handleResponse(json);`; default: @@ -470,12 +503,6 @@ function main() { const methods = extractMethods(sf); - const generated = methods.map(m => { - const name = m.name.text; - const config = inferReturnConfig(m.type, name, sf); - return generateMethod(name, m.parameters, config, sf); - }).join('\n\n'); - let client = fs.readFileSync(CLIENT_PATH, 'utf-8'); const beginIdx = client.indexOf(MARKER_BEGIN); @@ -486,8 +513,19 @@ function main() { } const before = client.slice(0, beginIdx + MARKER_BEGIN.length); + const existingRegion = client.slice(beginIdx + MARKER_BEGIN.length, endIdx); const after = client.slice(endIdx); + const generated = methods.map(m => { + const name = m.name.text; + if (PRESERVE_EXISTING_METHODS.has(name)) { + const existing = extractExistingTsMethod(existingRegion, name); + if (existing) return existing; + } + const config = inferReturnConfig(m.type, name, sf); + return generateMethod(name, m.parameters, config, sf); + }).join('\n\n'); + client = `${before}\n\n${generated}\n\n${after}`; fs.writeFileSync(CLIENT_PATH, client, 'utf-8');