pmxt-core 2.47.0 → 2.48.1

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 (68) 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/app.js +20 -3
  65. package/dist/server/method-verbs.json +10 -0
  66. package/dist/server/openapi.yaml +106 -0
  67. package/dist/types.d.ts +31 -0
  68. 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
+ }
@@ -384,13 +384,30 @@ function createApp(options = {}) {
384
384
  });
385
385
  // POST /api/:exchange/:method
386
386
  //
387
- // The original RPC-shaped surface. Body: { args: any[], credentials? }.
387
+ // Supports two calling conventions:
388
+ // - Envelope: { args: [...], credentials? } — original RPC shape, used by SDKs
389
+ // - Flat body: { slug: "wta", limit: 3, ... } — raw-curl / documentation examples
390
+ //
391
+ // When `args` is a valid array it is used directly (envelope path).
392
+ // When the body is a plain object without an `args` array, the body minus
393
+ // the reserved envelope keys (`args`, `credentials`) becomes args[0].
388
394
  // Accepts every method, including reads — so pre-existing clients
389
395
  // that POST reads keep working forever.
390
396
  app.post("/api/:exchange/:method", async (req, res, next) => {
391
397
  const methodName = req.params.method;
392
- const args = Array.isArray(req.body.args) ? req.body.args : [];
393
- const credentials = req.body.credentials;
398
+ const body = req.body;
399
+ const credentials = body.credentials;
400
+ let args;
401
+ if (Array.isArray(body.args)) {
402
+ args = body.args;
403
+ }
404
+ else if (body && typeof body === 'object' && !Array.isArray(body)) {
405
+ const { args: _ignored, credentials: _creds, ...rest } = body;
406
+ args = Object.keys(rest).length > 0 ? [rest] : [];
407
+ }
408
+ else {
409
+ args = [];
410
+ }
394
411
  await dispatchMethod(req, res, next, methodName, args, credentials);
395
412
  });
396
413
  // Error handler
@@ -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,31 @@ 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
+ - $ref: '#/components/parameters/ExchangeParam'
387
+ responses:
388
+ '200':
389
+ description: Fetch Series response
390
+ content:
391
+ application/json:
392
+ schema:
393
+ allOf:
394
+ - $ref: '#/components/schemas/BaseResponse'
395
+ - type: object
396
+ properties:
397
+ data:
398
+ type: array
399
+ items:
400
+ $ref: '#/components/schemas/UnifiedSeries'
401
+ description: >-
402
+ Fetch the recurring series (fourth tier above Event -> Market -> Outcome) that this venue exposes. Returns an
403
+ empty array on venues without a series concept (Limitless, Smarkets, Probable, Metaculus, Baozi, Hyperliquid,
404
+ SuiBets, Polymarket US). - `params.id` -> a single matching series with its events populated where supported. -
405
+ no params -> the full list, typically without nested events for payload size.
372
406
  '/api/{exchange}/fetchMarket':
373
407
  get:
374
408
  summary: Fetch Market
@@ -548,6 +582,15 @@ paths:
548
582
  schema:
549
583
  type: string
550
584
  description: Lookup by event slug
585
+ - in: query
586
+ name: series
587
+ required: false
588
+ schema:
589
+ type: string
590
+ description: >-
591
+ Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi
592
+ `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to
593
+ `sourceMetadata` after fetch.
551
594
  - in: query
552
595
  name: filter
553
596
  required: false
@@ -2780,6 +2823,63 @@ components:
2780
2823
  - markets
2781
2824
  - volume24h
2782
2825
  - url
2826
+ UnifiedSeries:
2827
+ type: object
2828
+ description: >-
2829
+ A recurring grouping of events on a venue — the fourth tier above Event -> Market -> Outcome. Examples: Kalshi
2830
+ `KXATPMATCH` (every ATP tennis match), Polymarket `wta` (every WTA match), Opinion's daily `collection`. Series
2831
+ only exists where the venue exposes a recurring-event concept; venues without one return an empty array from
2832
+ `fetchSeries`.
2833
+ properties:
2834
+ id:
2835
+ type: string
2836
+ description: >-
2837
+ Stable venue-native series identifier (e.g. "KXATPMATCH" on Kalshi, "atp" on Polymarket Gamma, numeric Gamma
2838
+ id).
2839
+ ticker:
2840
+ type: string
2841
+ description: 'Venue-native ticker, when distinct from `id`.'
2842
+ slug:
2843
+ type: string
2844
+ description: Venue-native slug.
2845
+ title:
2846
+ type: string
2847
+ description: 'Human-readable series title (e.g. "ATP Match Winner", "WTA").'
2848
+ description:
2849
+ oneOf:
2850
+ - type: string
2851
+ - {}
2852
+ description: Long-form series description.
2853
+ recurrence:
2854
+ oneOf:
2855
+ - type: string
2856
+ - {}
2857
+ description: 'Recurrence cadence the venue reports (''daily'', ''weekly'', ''annual'', ...).'
2858
+ events:
2859
+ type: array
2860
+ items:
2861
+ $ref: '#/components/schemas/UnifiedEvent'
2862
+ description: Child events. Populated when fetched by id; the list form usually omits this to keep payloads small.
2863
+ url:
2864
+ oneOf:
2865
+ - type: string
2866
+ - {}
2867
+ description: Canonical venue URL for the series.
2868
+ image:
2869
+ oneOf:
2870
+ - type: string
2871
+ - {}
2872
+ description: Venue-hosted image.
2873
+ sourceExchange:
2874
+ type: string
2875
+ description: The exchange this series originates from. Populated by the Router.
2876
+ sourceMetadata:
2877
+ type: object
2878
+ additionalProperties: {}
2879
+ description: Raw venue-specific fields not promoted to first-class columns.
2880
+ required:
2881
+ - id
2882
+ - title
2783
2883
  PriceCandle:
2784
2884
  type: object
2785
2885
  properties:
@@ -3173,6 +3273,12 @@ components:
3173
3273
  slug:
3174
3274
  type: string
3175
3275
  description: Lookup by event slug
3276
+ series:
3277
+ type: string
3278
+ description: >-
3279
+ Filter events by their parent series. Accepts the venue-native series id / ticker / slug (e.g. Kalshi
3280
+ `"KXATPMATCH"`, Polymarket `"wta"`). Passed through to the vendor where supported, otherwise applied to
3281
+ `sourceMetadata` after fetch.
3176
3282
  filter:
3177
3283
  allOf:
3178
3284
  - $ref: '#/components/schemas/EventFilterCriteria'