pmxt-core 2.47.0 → 2.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/BaseExchange.d.ts +42 -1
  2. package/dist/BaseExchange.js +29 -2
  3. package/dist/exchanges/baozi/index.d.ts +1 -0
  4. package/dist/exchanges/baozi/index.js +5 -0
  5. package/dist/exchanges/gemini-titan/index.d.ts +6 -2
  6. package/dist/exchanges/gemini-titan/index.js +81 -1
  7. package/dist/exchanges/gemini-titan/normalizer.d.ts +7 -1
  8. package/dist/exchanges/gemini-titan/normalizer.js +23 -0
  9. package/dist/exchanges/hyperliquid/index.d.ts +3 -0
  10. package/dist/exchanges/hyperliquid/index.js +7 -0
  11. package/dist/exchanges/kalshi/api.d.ts +1 -1
  12. package/dist/exchanges/kalshi/api.js +1 -1
  13. package/dist/exchanges/kalshi/fetcher.d.ts +10 -0
  14. package/dist/exchanges/kalshi/fetcher.js +36 -0
  15. package/dist/exchanges/kalshi/index.d.ts +3 -2
  16. package/dist/exchanges/kalshi/index.js +33 -0
  17. package/dist/exchanges/kalshi/normalizer.d.ts +3 -2
  18. package/dist/exchanges/kalshi/normalizer.js +14 -0
  19. package/dist/exchanges/limitless/api.d.ts +1 -1
  20. package/dist/exchanges/limitless/api.js +1 -1
  21. package/dist/exchanges/limitless/index.d.ts +1 -0
  22. package/dist/exchanges/limitless/index.js +5 -0
  23. package/dist/exchanges/metaculus/fetchEvents.js +4 -0
  24. package/dist/exchanges/metaculus/index.d.ts +3 -0
  25. package/dist/exchanges/metaculus/index.js +3 -0
  26. package/dist/exchanges/myriad/api.d.ts +1 -1
  27. package/dist/exchanges/myriad/api.js +1 -1
  28. package/dist/exchanges/myriad/index.d.ts +1 -0
  29. package/dist/exchanges/myriad/index.js +5 -0
  30. package/dist/exchanges/opinion/api.d.ts +1 -1
  31. package/dist/exchanges/opinion/api.js +1 -1
  32. package/dist/exchanges/opinion/index.d.ts +6 -2
  33. package/dist/exchanges/opinion/index.js +54 -1
  34. package/dist/exchanges/opinion/normalizer.d.ts +8 -2
  35. package/dist/exchanges/opinion/normalizer.js +16 -0
  36. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  37. package/dist/exchanges/polymarket/api-clob.js +1 -1
  38. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  39. package/dist/exchanges/polymarket/api-data.js +1 -1
  40. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  41. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  42. package/dist/exchanges/polymarket/index.d.ts +13 -2
  43. package/dist/exchanges/polymarket/index.js +49 -0
  44. package/dist/exchanges/polymarket/normalizer.d.ts +2 -1
  45. package/dist/exchanges/polymarket/normalizer.js +33 -0
  46. package/dist/exchanges/polymarket_us/index.d.ts +3 -2
  47. package/dist/exchanges/polymarket_us/index.js +57 -0
  48. package/dist/exchanges/polymarket_us/normalizer.d.ts +9 -2
  49. package/dist/exchanges/polymarket_us/normalizer.js +27 -0
  50. package/dist/exchanges/probable/api.d.ts +1 -1
  51. package/dist/exchanges/probable/api.js +1 -1
  52. package/dist/exchanges/probable/index.d.ts +3 -0
  53. package/dist/exchanges/probable/index.js +7 -0
  54. package/dist/exchanges/smarkets/index.d.ts +1 -0
  55. package/dist/exchanges/smarkets/index.js +5 -0
  56. package/dist/exchanges/suibets/index.d.ts +1 -0
  57. package/dist/exchanges/suibets/index.js +5 -0
  58. package/dist/router/Router.d.ts +29 -2
  59. package/dist/router/Router.js +145 -0
  60. package/dist/router/index.d.ts +1 -0
  61. package/dist/router/index.js +1 -0
  62. package/dist/router/series-map.d.ts +32 -0
  63. package/dist/router/series-map.js +146 -0
  64. package/dist/server/method-verbs.json +10 -0
  65. package/dist/server/openapi.yaml +118 -0
  66. package/dist/types.d.ts +31 -0
  67. package/package.json +3 -3
@@ -2,6 +2,9 @@ import { PredictionMarketExchange, MarketFetchParams, EventFetchParams, Exchange
2
2
  import { UnifiedMarket, UnifiedEvent, OrderBook, PriceCandle, Trade, UserTrade, Order, Position, Balance, CreateOrderParams } from '../../types';
3
3
  import { ProbableWebSocketConfig } from './websocket';
4
4
  export declare class ProbableExchange extends PredictionMarketExchange {
5
+ protected readonly capabilityOverrides: {
6
+ fetchSeries: false;
7
+ };
5
8
  private auth?;
6
9
  private ws?;
7
10
  private wsConfig?;
@@ -16,6 +16,9 @@ const normalizer_1 = require("./normalizer");
16
16
  const logger_1 = require("../../utils/logger");
17
17
  const BSC_USDT_ADDRESS = '0x55d398326f99059fF775485246999027B3197955';
18
18
  class ProbableExchange extends BaseExchange_1.PredictionMarketExchange {
19
+ capabilityOverrides = {
20
+ fetchSeries: false,
21
+ };
19
22
  auth;
20
23
  ws;
21
24
  wsConfig;
@@ -78,6 +81,10 @@ class ProbableExchange extends BaseExchange_1.PredictionMarketExchange {
78
81
  return filtered;
79
82
  }
80
83
  async fetchEventsImpl(params) {
84
+ // Venue does not expose a series concept; honoring `params.series` by
85
+ // returning [] rather than ignoring the filter.
86
+ if (params.series !== undefined)
87
+ return [];
81
88
  const rawEvents = await this.fetcher.fetchRawEvents(params);
82
89
  const events = rawEvents
83
90
  .map((raw) => this.normalizer.normalizeEvent(raw))
@@ -3,6 +3,7 @@ import { UnifiedMarket, UnifiedEvent, OrderBook, Trade, UserTrade, Balance, Orde
3
3
  export declare class SmarketsExchange extends PredictionMarketExchange {
4
4
  protected readonly capabilityOverrides: {
5
5
  fetchPositions: "emulated";
6
+ fetchSeries: false;
6
7
  };
7
8
  private auth?;
8
9
  private loginPromise;
@@ -16,6 +16,7 @@ const logger_1 = require("../../utils/logger");
16
16
  class SmarketsExchange extends BaseExchange_1.PredictionMarketExchange {
17
17
  capabilityOverrides = {
18
18
  fetchPositions: 'emulated',
19
+ fetchSeries: false,
19
20
  };
20
21
  auth;
21
22
  loginPromise = null;
@@ -149,6 +150,10 @@ class SmarketsExchange extends BaseExchange_1.PredictionMarketExchange {
149
150
  return allMarkets.slice(offset, offset + limit);
150
151
  }
151
152
  async fetchEventsImpl(params) {
153
+ // Venue does not expose a series concept; honoring `params.series` by
154
+ // returning [] rather than ignoring the filter.
155
+ if (params.series !== undefined)
156
+ return [];
152
157
  const rawEvents = await this.fetcher.fetchRawEvents(params);
153
158
  const limit = params?.limit || 250000;
154
159
  const query = (params?.query || '').toLowerCase();
@@ -33,6 +33,7 @@ export declare class SuiBetsExchange extends PredictionMarketExchange {
33
33
  fetchPositions: true;
34
34
  watchOrderBook: false;
35
35
  watchTrades: false;
36
+ fetchSeries: false;
36
37
  };
37
38
  private readonly config;
38
39
  private readonly fetcher;
@@ -34,6 +34,7 @@ class SuiBetsExchange extends BaseExchange_1.PredictionMarketExchange {
34
34
  fetchPositions: true,
35
35
  watchOrderBook: false,
36
36
  watchTrades: false,
37
+ fetchSeries: false,
37
38
  };
38
39
  config;
39
40
  fetcher;
@@ -72,6 +73,10 @@ class SuiBetsExchange extends BaseExchange_1.PredictionMarketExchange {
72
73
  .filter((m) => m !== null);
73
74
  }
74
75
  async fetchEventsImpl(params) {
76
+ // Venue does not expose a series concept; honoring `params.series` by returning [] rather than ignoring the filter.
77
+ if (params.series !== undefined) {
78
+ return [];
79
+ }
75
80
  const raw = await this.fetcher.fetchRawEvents(params);
76
81
  return raw
77
82
  .map(r => this.normalizer.normalizeEvent(r))
@@ -1,5 +1,5 @@
1
- import { PredictionMarketExchange, type MarketFetchParams, type EventFetchParams } from '../BaseExchange';
2
- import type { UnifiedMarket, UnifiedEvent, OrderBook } from '../types';
1
+ import { PredictionMarketExchange, type MarketFetchParams, type EventFetchParams, type SeriesFetchParams } from '../BaseExchange';
2
+ import type { UnifiedMarket, UnifiedEvent, UnifiedSeries, OrderBook } from '../types';
3
3
  import type { RouterOptions, MatchResult, EventMatchResult, PriceComparison, ArbitrageOpportunity, MatchedMarketPair, MatchedPricePair, FetchMarketMatchesParams, FetchMatchesParams, FetchEventMatchesParams, FetchArbitrageParams, FetchMatchedMarketsParams, FetchMatchedPricesParams } from './types';
4
4
  export declare class Router extends PredictionMarketExchange {
5
5
  private readonly client;
@@ -9,6 +9,33 @@ export declare class Router extends PredictionMarketExchange {
9
9
  get name(): string;
10
10
  protected fetchMarketsImpl(params?: MarketFetchParams): Promise<UnifiedMarket[]>;
11
11
  protected fetchEventsImpl(params?: EventFetchParams): Promise<UnifiedEvent[]>;
12
+ /**
13
+ * Fan out fetchEvents({series: venueSeriesId}) to each venue in the
14
+ * mapping, collect all results, and tag each event with its sourceExchange.
15
+ */
16
+ private fetchEventsForMappedSeries;
17
+ /**
18
+ * Cross-venue series fetch.
19
+ *
20
+ * - If `params.id` matches a normalized id in SERIES_MAP, returns a single
21
+ * synthesized `UnifiedSeries` whose `events` is the concatenation of
22
+ * `fetchEvents({series: venueSeriesId})` results from each mapped venue.
23
+ * - Otherwise fans out `fetchSeries(params)` to all venue instances with
24
+ * `has.fetchSeries !== false`, collects, and returns deduplicated results
25
+ * tagged with their `sourceExchange`.
26
+ */
27
+ protected fetchSeriesImpl(params: SeriesFetchParams): Promise<UnifiedSeries[]>;
28
+ /**
29
+ * Build a single synthesized `UnifiedSeries` from the SERIES_MAP entry by
30
+ * fetching events from all mapped venues and concatenating them.
31
+ */
32
+ private fetchSynthesizedSeries;
33
+ /**
34
+ * Fan out `fetchSeries(params)` to all venue instances whose
35
+ * `has.fetchSeries` is not `false`. Tag each result with its originating
36
+ * venue name.
37
+ */
38
+ private fanOutFetchSeries;
12
39
  fetchOrderBook(outcomeId: string, limit?: number, params?: Record<string, any>): Promise<OrderBook>;
13
40
  fetchMarketMatches(params?: FetchMarketMatchesParams): Promise<MatchResult[]>;
14
41
  /**
@@ -5,6 +5,7 @@ const BaseExchange_1 = require("../BaseExchange");
5
5
  const errors_1 = require("../errors");
6
6
  const logger_1 = require("../utils/logger");
7
7
  const client_1 = require("./client");
8
+ const series_map_1 = require("./series-map");
8
9
  // ---------------------------------------------------------------------------
9
10
  // Orderbook merge utilities
10
11
  // ---------------------------------------------------------------------------
@@ -127,6 +128,18 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
127
128
  return response;
128
129
  }
129
130
  async fetchEventsImpl(params) {
131
+ // When a normalized series id is requested, fan out to each mapped venue
132
+ // using the venue-native series id and aggregate the results.
133
+ if (params?.series !== undefined) {
134
+ const normalized = params.series;
135
+ const entry = series_map_1.SERIES_MAP.find((e) => e.id === normalized);
136
+ if (entry !== undefined) {
137
+ return this.fetchEventsForMappedSeries(entry.venues, params);
138
+ }
139
+ // Not a known normalized id — fall through and pass the raw value to
140
+ // the hosted search endpoint (single-venue callers using vendor-native
141
+ // ids still work).
142
+ }
130
143
  const response = await this.client.searchEvents({
131
144
  query: params?.query,
132
145
  category: params?.category,
@@ -138,6 +151,138 @@ class Router extends BaseExchange_1.PredictionMarketExchange {
138
151
  }
139
152
  return response;
140
153
  }
154
+ /**
155
+ * Fan out fetchEvents({series: venueSeriesId}) to each venue in the
156
+ * mapping, collect all results, and tag each event with its sourceExchange.
157
+ */
158
+ async fetchEventsForMappedSeries(venueMap, baseParams) {
159
+ const venueEntries = Object.entries(venueMap);
160
+ if (venueEntries.length === 0)
161
+ return [];
162
+ const fetchResults = await Promise.all(venueEntries.map(async ([venueName, venueSeriesId]) => {
163
+ const exchange = this.localExchanges[venueName];
164
+ if (!exchange) {
165
+ logger_1.logger.debug(`Router.fetchEventsForMappedSeries: no exchange instance for venue "${venueName}", skipping`);
166
+ return [];
167
+ }
168
+ try {
169
+ const events = await exchange.fetchEvents({
170
+ ...baseParams,
171
+ series: venueSeriesId,
172
+ });
173
+ return events.map((ev) => ({
174
+ ...ev,
175
+ sourceExchange: ev.sourceExchange ?? venueName,
176
+ }));
177
+ }
178
+ catch (error) {
179
+ logger_1.logger.warn(`Router.fetchEventsForMappedSeries: fetchEvents failed for venue "${venueName}" ` +
180
+ `series "${venueSeriesId}": ${error instanceof Error ? error.message : String(error)}`);
181
+ return [];
182
+ }
183
+ }));
184
+ return fetchResults.flat();
185
+ }
186
+ // -----------------------------------------------------------------------
187
+ // Cross-venue series (series-map + fan-out)
188
+ // -----------------------------------------------------------------------
189
+ /**
190
+ * Cross-venue series fetch.
191
+ *
192
+ * - If `params.id` matches a normalized id in SERIES_MAP, returns a single
193
+ * synthesized `UnifiedSeries` whose `events` is the concatenation of
194
+ * `fetchEvents({series: venueSeriesId})` results from each mapped venue.
195
+ * - Otherwise fans out `fetchSeries(params)` to all venue instances with
196
+ * `has.fetchSeries !== false`, collects, and returns deduplicated results
197
+ * tagged with their `sourceExchange`.
198
+ */
199
+ async fetchSeriesImpl(params) {
200
+ const requestedId = params.id;
201
+ if (requestedId !== undefined) {
202
+ const entry = series_map_1.SERIES_MAP.find((e) => e.id === requestedId);
203
+ if (entry !== undefined) {
204
+ return this.fetchSynthesizedSeries(entry, params);
205
+ }
206
+ }
207
+ return this.fanOutFetchSeries(params);
208
+ }
209
+ /**
210
+ * Build a single synthesized `UnifiedSeries` from the SERIES_MAP entry by
211
+ * fetching events from all mapped venues and concatenating them.
212
+ */
213
+ async fetchSynthesizedSeries(entry, params) {
214
+ const venueEntries = Object.entries(entry.venues);
215
+ const eventsPerVenue = await Promise.all(venueEntries.map(async ([venueName, venueSeriesId]) => {
216
+ const exchange = this.localExchanges[venueName];
217
+ if (!exchange) {
218
+ logger_1.logger.debug(`Router.fetchSynthesizedSeries: no exchange instance for venue "${venueName}", skipping`);
219
+ return [];
220
+ }
221
+ try {
222
+ const events = await exchange.fetchEvents({ series: venueSeriesId });
223
+ return events.map((ev) => ({
224
+ ...ev,
225
+ sourceExchange: ev.sourceExchange ?? venueName,
226
+ }));
227
+ }
228
+ catch (error) {
229
+ logger_1.logger.warn(`Router.fetchSynthesizedSeries: fetchEvents failed for venue "${venueName}" ` +
230
+ `series "${venueSeriesId}": ${error instanceof Error ? error.message : String(error)}`);
231
+ return [];
232
+ }
233
+ }));
234
+ const allEvents = eventsPerVenue.flat();
235
+ const synthesized = {
236
+ id: entry.id,
237
+ title: entry.title,
238
+ events: allEvents,
239
+ sourceExchange: 'Router',
240
+ };
241
+ // Apply limit/offset if the caller passed them (BaseExchange.fetchSeries strips
242
+ // them before calling fetchSeriesImpl, but guard defensively).
243
+ const limit = params.limit;
244
+ const offset = params.offset ?? 0;
245
+ if (limit !== undefined) {
246
+ return [{ ...synthesized, events: allEvents.slice(offset, offset + limit) }];
247
+ }
248
+ return [synthesized];
249
+ }
250
+ /**
251
+ * Fan out `fetchSeries(params)` to all venue instances whose
252
+ * `has.fetchSeries` is not `false`. Tag each result with its originating
253
+ * venue name.
254
+ */
255
+ async fanOutFetchSeries(params) {
256
+ const venueEntries = Object.entries(this.localExchanges);
257
+ if (venueEntries.length === 0)
258
+ return [];
259
+ const fetchResults = await Promise.all(venueEntries.map(async ([venueName, exchange]) => {
260
+ if (exchange.has.fetchSeries === false)
261
+ return [];
262
+ // When params.id is a venue-native id on this specific venue,
263
+ // pass it through directly so single-venue lookups still work.
264
+ let venueParams = params;
265
+ if (params.id !== undefined) {
266
+ const nativeId = (0, series_map_1.getVenueSeriesId)(params.id, venueName);
267
+ if (nativeId !== undefined) {
268
+ venueParams = { ...params, id: nativeId };
269
+ }
270
+ }
271
+ try {
272
+ const series = await exchange.fetchSeries(venueParams);
273
+ return series.map((s) => ({
274
+ ...s,
275
+ sourceExchange: s.sourceExchange ?? venueName,
276
+ }));
277
+ }
278
+ catch (error) {
279
+ logger_1.logger.warn(`Router.fanOutFetchSeries: fetchSeries failed for venue "${venueName}": ` +
280
+ `${error instanceof Error ? error.message : String(error)}`);
281
+ return [];
282
+ }
283
+ }));
284
+ return fetchResults.flat();
285
+ }
141
286
  // -----------------------------------------------------------------------
142
287
  // Unified orderbook (cross-exchange merge)
143
288
  // -----------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
1
  export { Router } from './Router';
2
2
  export { PmxtApiClient } from './client';
3
3
  export * from './types';
4
+ export * from './series-map';
@@ -20,3 +20,4 @@ Object.defineProperty(exports, "Router", { enumerable: true, get: function () {
20
20
  var client_1 = require("./client");
21
21
  Object.defineProperty(exports, "PmxtApiClient", { enumerable: true, get: function () { return client_1.PmxtApiClient; } });
22
22
  __exportStar(require("./types"), exports);
23
+ __exportStar(require("./series-map"), exports);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Curated mapping from normalized PMXT series ids to per-venue native series ids.
3
+ *
4
+ * The normalized ids use kebab-case following the pattern:
5
+ * <sport-or-domain>-<category-or-format>
6
+ *
7
+ * Venue-native ids are the raw tickers/slugs each platform uses.
8
+ * It is intentional for some entries to have only partial venue coverage --
9
+ * the Router handles missing venue mappings gracefully by skipping that venue.
10
+ */
11
+ export interface RouterSeriesEntry {
12
+ /** Normalized PMXT series id (kebab-case). */
13
+ id: string;
14
+ /** Human-readable title. */
15
+ title: string;
16
+ /** Map of venueName -> venue-native series id. */
17
+ venues: {
18
+ readonly [venueName: string]: string;
19
+ };
20
+ }
21
+ export declare const SERIES_MAP: readonly RouterSeriesEntry[];
22
+ /**
23
+ * Resolve a normalized PMXT series id to the venue-native series id for a
24
+ * given venue. Returns `undefined` when either the normalized id is not in the
25
+ * map or that venue has no mapping for it.
26
+ */
27
+ export declare function getVenueSeriesId(normalizedId: string, venue: string): string | undefined;
28
+ /**
29
+ * Reverse-lookup: given a venue name and a venue-native series id, return the
30
+ * normalized PMXT series id. Returns `undefined` when not found.
31
+ */
32
+ export declare function getNormalizedSeriesId(venue: string, venueSeriesId: string): string | undefined;
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ /**
3
+ * Curated mapping from normalized PMXT series ids to per-venue native series ids.
4
+ *
5
+ * The normalized ids use kebab-case following the pattern:
6
+ * <sport-or-domain>-<category-or-format>
7
+ *
8
+ * Venue-native ids are the raw tickers/slugs each platform uses.
9
+ * It is intentional for some entries to have only partial venue coverage --
10
+ * the Router handles missing venue mappings gracefully by skipping that venue.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.SERIES_MAP = void 0;
14
+ exports.getVenueSeriesId = getVenueSeriesId;
15
+ exports.getNormalizedSeriesId = getNormalizedSeriesId;
16
+ exports.SERIES_MAP = [
17
+ {
18
+ id: 'tennis-atp-match',
19
+ title: 'ATP Match Winner',
20
+ venues: {
21
+ kalshi: 'KXATPSETWINNER',
22
+ polymarket: 'atp',
23
+ },
24
+ },
25
+ {
26
+ id: 'tennis-atp-challenger',
27
+ title: 'ATP Challenger Match Winner',
28
+ venues: {
29
+ kalshi: 'KXATPCHALLENGERMATCH',
30
+ },
31
+ },
32
+ {
33
+ id: 'tennis-wta-match',
34
+ title: 'WTA Match Winner',
35
+ venues: {
36
+ kalshi: 'KXWTASETWINNER',
37
+ polymarket: 'wta',
38
+ },
39
+ },
40
+ {
41
+ id: 'tennis-itf-match',
42
+ title: 'ITF Match Winner',
43
+ venues: {
44
+ kalshi: 'KXITFMATCH',
45
+ },
46
+ },
47
+ {
48
+ id: 'tennis-itf-women-match',
49
+ title: "ITF Women's Match Winner",
50
+ venues: {
51
+ kalshi: 'KXITFWMATCH',
52
+ },
53
+ },
54
+ {
55
+ id: 'nfl',
56
+ title: 'NFL Game Winner',
57
+ venues: {
58
+ kalshi: 'KXNFLGAME',
59
+ polymarket: 'nfl-game',
60
+ },
61
+ },
62
+ {
63
+ id: 'nba',
64
+ title: 'NBA Game Winner',
65
+ venues: {
66
+ kalshi: 'KXNBAGAME',
67
+ polymarket: 'nba',
68
+ },
69
+ },
70
+ {
71
+ id: 'ncaa-basketball',
72
+ title: 'NCAA Basketball Game Winner',
73
+ venues: {
74
+ kalshi: 'KXNCAABBGAME',
75
+ },
76
+ },
77
+ {
78
+ id: 'ufc',
79
+ title: 'UFC Fight Winner',
80
+ venues: {
81
+ polymarket: 'ufc',
82
+ },
83
+ },
84
+ {
85
+ id: 'soccer-fifa-world-cup',
86
+ title: 'FIFA World Cup Match Winner',
87
+ venues: {
88
+ polymarket: 'soccer-fifwc',
89
+ },
90
+ },
91
+ {
92
+ id: 'esports-cs2-map',
93
+ title: 'CS2 Map Winner',
94
+ venues: {
95
+ kalshi: 'KXCS2MAP',
96
+ },
97
+ },
98
+ {
99
+ id: 'esports-lol-map',
100
+ title: 'League of Legends Map Winner',
101
+ venues: {
102
+ kalshi: 'KXLOLMAP',
103
+ },
104
+ },
105
+ {
106
+ id: 'crypto-btc-15m',
107
+ title: 'Bitcoin Price (15-minute)',
108
+ venues: {
109
+ kalshi: 'KXBTC15M',
110
+ },
111
+ },
112
+ {
113
+ id: 'crypto-eth-15m',
114
+ title: 'Ethereum Price (15-minute)',
115
+ venues: {
116
+ kalshi: 'KXETH15M',
117
+ },
118
+ },
119
+ {
120
+ id: 'crypto-sol-15m',
121
+ title: 'Solana Price (15-minute)',
122
+ venues: {
123
+ kalshi: 'KXSOL15M',
124
+ },
125
+ },
126
+ ];
127
+ // ---------------------------------------------------------------------------
128
+ // Lookup helpers (O(n) over small constant array; acceptable for this table)
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * Resolve a normalized PMXT series id to the venue-native series id for a
132
+ * given venue. Returns `undefined` when either the normalized id is not in the
133
+ * map or that venue has no mapping for it.
134
+ */
135
+ function getVenueSeriesId(normalizedId, venue) {
136
+ const entry = exports.SERIES_MAP.find((e) => e.id === normalizedId);
137
+ return entry?.venues[venue];
138
+ }
139
+ /**
140
+ * Reverse-lookup: given a venue name and a venue-native series id, return the
141
+ * normalized PMXT series id. Returns `undefined` when not found.
142
+ */
143
+ function getNormalizedSeriesId(venue, venueSeriesId) {
144
+ const entry = exports.SERIES_MAP.find((e) => e.venues[venue] === venueSeriesId);
145
+ return entry?.id;
146
+ }
@@ -49,6 +49,16 @@
49
49
  }
50
50
  ]
51
51
  },
52
+ "fetchSeries": {
53
+ "verb": "get",
54
+ "args": [
55
+ {
56
+ "name": "params",
57
+ "kind": "object",
58
+ "optional": true
59
+ }
60
+ ]
61
+ },
52
62
  "fetchMarket": {
53
63
  "verb": "get",
54
64
  "args": [
@@ -326,6 +326,15 @@ paths:
326
326
  schema:
327
327
  type: string
328
328
  description: Lookup by event slug
329
+ - in: query
330
+ name: series
331
+ required: false
332
+ schema:
333
+ type: string
334
+ description: >-
335
+ Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi
336
+ `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to
337
+ `sourceMetadata` after fetch.
329
338
  - in: query
330
339
  name: filter
331
340
  required: false
@@ -369,6 +378,43 @@ paths:
369
378
  description: >-
370
379
  Fetch events with optional keyword search. Events group related markets together (e.g., "Who will be Fed Chair?"
371
380
  contains multiple candidate markets).
381
+ '/api/{exchange}/fetchSeries':
382
+ get:
383
+ summary: Fetch Series
384
+ operationId: fetchSeries
385
+ parameters:
386
+ - in: path
387
+ name: exchange
388
+ schema:
389
+ type: string
390
+ enum:
391
+ - polymarket
392
+ - kalshi
393
+ - kalshi-demo
394
+ - opinion
395
+ - polymarket_us
396
+ - router
397
+ required: true
398
+ description: The prediction market exchange to target.
399
+ responses:
400
+ '200':
401
+ description: Fetch Series response
402
+ content:
403
+ application/json:
404
+ schema:
405
+ allOf:
406
+ - $ref: '#/components/schemas/BaseResponse'
407
+ - type: object
408
+ properties:
409
+ data:
410
+ type: array
411
+ items:
412
+ $ref: '#/components/schemas/UnifiedSeries'
413
+ description: >-
414
+ Fetch the recurring series (fourth tier above Event -> Market -> Outcome) that this venue exposes. Returns an
415
+ empty array on venues without a series concept (Limitless, Smarkets, Probable, Metaculus, Baozi, Hyperliquid,
416
+ SuiBets, Polymarket US). - `params.id` -> a single matching series with its events populated where supported. -
417
+ no params -> the full list, typically without nested events for payload size.
372
418
  '/api/{exchange}/fetchMarket':
373
419
  get:
374
420
  summary: Fetch Market
@@ -548,6 +594,15 @@ paths:
548
594
  schema:
549
595
  type: string
550
596
  description: Lookup by event slug
597
+ - in: query
598
+ name: series
599
+ required: false
600
+ schema:
601
+ type: string
602
+ description: >-
603
+ Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi
604
+ `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to
605
+ `sourceMetadata` after fetch.
551
606
  - in: query
552
607
  name: filter
553
608
  required: false
@@ -2780,6 +2835,63 @@ components:
2780
2835
  - markets
2781
2836
  - volume24h
2782
2837
  - url
2838
+ UnifiedSeries:
2839
+ type: object
2840
+ description: >-
2841
+ A recurring grouping of events on a venue — the fourth tier above Event -> Market -> Outcome. Examples: Kalshi
2842
+ `KXATPMATCH` (every ATP tennis match), Polymarket `wta` (every WTA match), Opinion's daily `collection`. Series
2843
+ only exists where the venue exposes a recurring-event concept; venues without one return an empty array from
2844
+ `fetchSeries`.
2845
+ properties:
2846
+ id:
2847
+ type: string
2848
+ description: >-
2849
+ Stable venue-native series identifier (e.g. "KXATPMATCH" on Kalshi, "atp" on Polymarket Gamma, numeric Gamma
2850
+ id).
2851
+ ticker:
2852
+ type: string
2853
+ description: 'Venue-native ticker, when distinct from `id`.'
2854
+ slug:
2855
+ type: string
2856
+ description: Venue-native slug.
2857
+ title:
2858
+ type: string
2859
+ description: 'Human-readable series title (e.g. "ATP Match Winner", "WTA").'
2860
+ description:
2861
+ oneOf:
2862
+ - type: string
2863
+ - {}
2864
+ description: Long-form series description.
2865
+ recurrence:
2866
+ oneOf:
2867
+ - type: string
2868
+ - {}
2869
+ description: 'Recurrence cadence the venue reports (''daily'', ''weekly'', ''annual'', ...).'
2870
+ events:
2871
+ type: array
2872
+ items:
2873
+ $ref: '#/components/schemas/UnifiedEvent'
2874
+ description: Child events. Populated when fetched by id; the list form usually omits this to keep payloads small.
2875
+ url:
2876
+ oneOf:
2877
+ - type: string
2878
+ - {}
2879
+ description: Canonical venue URL for the series.
2880
+ image:
2881
+ oneOf:
2882
+ - type: string
2883
+ - {}
2884
+ description: Venue-hosted image.
2885
+ sourceExchange:
2886
+ type: string
2887
+ description: The exchange this series originates from. Populated by the Router.
2888
+ sourceMetadata:
2889
+ type: object
2890
+ additionalProperties: {}
2891
+ description: Raw venue-specific fields not promoted to first-class columns.
2892
+ required:
2893
+ - id
2894
+ - title
2783
2895
  PriceCandle:
2784
2896
  type: object
2785
2897
  properties:
@@ -3173,6 +3285,12 @@ components:
3173
3285
  slug:
3174
3286
  type: string
3175
3287
  description: Lookup by event slug
3288
+ series:
3289
+ type: string
3290
+ description: >-
3291
+ Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi
3292
+ `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to
3293
+ `sourceMetadata` after fetch.
3176
3294
  filter:
3177
3295
  allOf:
3178
3296
  - $ref: '#/components/schemas/EventFilterCriteria'