pmxt-core 2.51.1 → 2.51.3

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 (46) hide show
  1. package/API_REFERENCE.md +17 -28
  2. package/bin/pmxt-ensure-server +22 -1
  3. package/bin/pmxt-ensure-server.js +64 -3
  4. package/dist/BaseExchange.d.ts +34 -0
  5. package/dist/BaseExchange.js +44 -2
  6. package/dist/exchanges/hunch/fetcher.d.ts +4 -0
  7. package/dist/exchanges/hunch/fetcher.js +19 -6
  8. package/dist/exchanges/hunch/utils.d.ts +8 -0
  9. package/dist/exchanges/hunch/utils.js +61 -7
  10. package/dist/exchanges/hyperliquid/normalizer.js +1 -1
  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.js +13 -0
  14. package/dist/exchanges/limitless/api.d.ts +1 -1
  15. package/dist/exchanges/limitless/api.js +1 -1
  16. package/dist/exchanges/limitless/index.js +4 -1
  17. package/dist/exchanges/metaculus/index.js +2 -1
  18. package/dist/exchanges/mock/index.d.ts +1 -1
  19. package/dist/exchanges/mock/index.js +37 -3
  20. package/dist/exchanges/myriad/api.d.ts +1 -1
  21. package/dist/exchanges/myriad/api.js +1 -1
  22. package/dist/exchanges/myriad/index.js +1 -1
  23. package/dist/exchanges/opinion/api.d.ts +1 -1
  24. package/dist/exchanges/opinion/api.js +1 -1
  25. package/dist/exchanges/opinion/index.js +1 -1
  26. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  27. package/dist/exchanges/polymarket/api-clob.js +1 -1
  28. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  29. package/dist/exchanges/polymarket/api-data.js +1 -1
  30. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  31. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  32. package/dist/exchanges/polymarket_us/config.js +4 -2
  33. package/dist/exchanges/probable/api.d.ts +1 -1
  34. package/dist/exchanges/probable/api.js +1 -1
  35. package/dist/exchanges/probable/index.js +1 -1
  36. package/dist/exchanges/rain/fetcher.d.ts +2 -1
  37. package/dist/exchanges/rain/fetcher.js +5 -6
  38. package/dist/exchanges/rain/index.js +5 -1
  39. package/dist/exchanges/smarkets/config.js +2 -1
  40. package/dist/exchanges/smarkets/fetcher.js +16 -6
  41. package/dist/server/app.d.ts +9 -0
  42. package/dist/server/app.js +34 -16
  43. package/dist/server/exchange-factory.js +9 -3
  44. package/dist/server/openapi.yaml +14 -2
  45. package/dist/types.d.ts +2 -0
  46. package/package.json +3 -3
package/API_REFERENCE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Prediction Market API Reference
2
2
 
3
- A unified interface for interacting with multiple prediction market exchanges (Kalshi, Polymarket) identically.
3
+ A unified interface for interacting with multiple prediction market exchanges and venues through one interface.
4
4
 
5
5
  ## Installation & Usage
6
6
 
@@ -8,33 +8,22 @@ A unified interface for interacting with multiple prediction market exchanges (K
8
8
  npm install pmxtjs
9
9
  ```
10
10
 
11
- ### Basic Import (CommonJS)
11
+ ### Import Styles
12
12
 
13
13
  ```typescript
14
- import pmxt from 'pmxtjs';
14
+ import { Polymarket, Kalshi } from 'pmxtjs';
15
15
 
16
- const poly = new pmxt.Polymarket();
17
- const kalshi = new pmxt.Kalshi();
16
+ const polymarket = new Polymarket();
17
+ const kalshi = new Kalshi();
18
18
  ```
19
19
 
20
- ### Note for ESM Users
21
-
22
- **pmxt is currently CommonJS-only.** If you're using `"type": "module"` in your `package.json`, you have two options:
23
-
24
- **Option 1: Default import (recommended)**
25
20
  ```typescript
26
21
  import pmxt from 'pmxtjs';
27
- const poly = new pmxt.Polymarket();
28
- ```
29
22
 
30
- **Option 2: Dynamic import**
31
- ```typescript
32
- const pmxt = await import('pmxtjs');
33
- const poly = new pmxt.default.Polymarket();
23
+ const polymarket = new pmxt.Polymarket();
24
+ const kalshi = new pmxt.Kalshi();
34
25
  ```
35
26
 
36
- **Note**: Named exports like `import { Polymarket } from 'pmxtjs'` will **not work** in ESM projects.
37
-
38
27
  ---
39
28
 
40
29
  ## Core Methods
@@ -46,8 +35,8 @@ Get active markets from an exchange. This is the unified method for all market f
46
35
  - `limit`: Number of results (default: 20)
47
36
  - `offset`: Pagination offset
48
37
  - `sort`: 'volume' | 'liquidity' | 'newest'
49
- - `query`: Search text (replaces `searchMarkets`)
50
- - `slug`: Market slug or ticker (replaces `getMarketsBySlug`)
38
+ - `query`: Search text
39
+ - `slug`: Market slug or ticker
51
40
 
52
41
  ```typescript
53
42
  // 1. Fetch recent markets
@@ -56,13 +45,13 @@ const markets = await polymarket.fetchMarkets({
56
45
  sort: 'volume'
57
46
  });
58
47
 
59
- // 2. Search by text (replaces searchMarkets)
48
+ // 2. Search by text
60
49
  const results = await kalshi.fetchMarkets({
61
50
  query: 'Fed rates',
62
51
  limit: 10
63
52
  });
64
53
 
65
- // 3. Fetch by slug/ticker (replaces getMarketsBySlug)
54
+ // 3. Fetch by slug/ticker
66
55
  const market = await polymarket.fetchMarkets({
67
56
  slug: 'who-will-trump-nominate-as-fed-chair'
68
57
  });
@@ -140,7 +129,7 @@ Calculate the volume-weighted average price for a given amount. Returns `0` if t
140
129
 
141
130
  ```typescript
142
131
  const orderBook = await polymarket.fetchOrderBook(outcomeId);
143
- const price = await polymarket.getExecutionPrice(orderBook, 'buy', 100);
132
+ const price = polymarket.getExecutionPrice(orderBook, 'buy', 100);
144
133
  console.log(`Average price for 100 shares: ${price}`);
145
134
  ```
146
135
 
@@ -148,7 +137,7 @@ console.log(`Average price for 100 shares: ${price}`);
148
137
  Calculate detailed execution price information, including partial fills.
149
138
 
150
139
  ```typescript
151
- const detailed = await polymarket.getExecutionPriceDetailed(orderBook, 'buy', 100);
140
+ const detailed = polymarket.getExecutionPriceDetailed(orderBook, 'buy', 100);
152
141
  console.log(`Average Price: ${detailed.price}`);
153
142
  console.log(`Filled Amount: ${detailed.filledAmount}`);
154
143
  console.log(`Fully Filled: ${detailed.fullyFilled}`);
@@ -275,10 +264,10 @@ console.log(`Spread: ${(spread * 100).toFixed(2)}%`);
275
264
 
276
265
  ```typescript
277
266
  try {
278
- const markets = await kalshi.getMarketsBySlug('INVALID-TICKER');
267
+ const market = await kalshi.fetchMarket({ slug: 'INVALID-TICKER' });
268
+ console.log(market.title);
279
269
  } catch (error) {
280
- // Kalshi: "Event not found: INVALID-TICKER"
281
- // Polymarket: Returns empty array []
270
+ // Throws MarketNotFound when no market matches the lookup
282
271
  console.error(error.message);
283
272
  }
284
273
  ```
@@ -310,7 +299,7 @@ import pmxt, {
310
299
 
311
300
  ## Authentication & Trading
312
301
 
313
- Both Polymarket and Kalshi support authenticated trading operations. You must provide credentials when initializing the exchange.
302
+ Authenticated trading is available on venues that expose trading APIs. You must provide the venue-specific credentials when initializing the exchange; the examples below show Polymarket and Kalshi.
314
303
 
315
304
  ### Polymarket Authentication
316
305
 
@@ -39,7 +39,7 @@ function isServerRunning() {
39
39
  // Check if process exists
40
40
  try {
41
41
  process.kill(pid, 0); // Signal 0 checks existence without killing
42
- return { running: true, port };
42
+ return { running: true, pid, port };
43
43
  } catch (err) {
44
44
  // Process doesn't exist, remove stale lock file
45
45
  fs.unlinkSync(LOCK_FILE);
@@ -50,6 +50,26 @@ function isServerRunning() {
50
50
  }
51
51
  }
52
52
 
53
+ /**
54
+ * Remove the lock we just proved unhealthy, without deleting a newer sidecar's
55
+ * lock if another launcher won the race and replaced it first.
56
+ */
57
+ function removeLockFileIfMatches(staleLock) {
58
+ try {
59
+ if (!fs.existsSync(LOCK_FILE)) {
60
+ return false;
61
+ }
62
+ const currentLock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
63
+ if (currentLock.pid === staleLock.pid && currentLock.port === staleLock.port) {
64
+ fs.unlinkSync(LOCK_FILE);
65
+ return true;
66
+ }
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ return false;
71
+ }
72
+
53
73
  /**
54
74
  * Wait for server to respond to health check
55
75
  */
@@ -196,6 +216,7 @@ async function main() {
196
216
  } catch (err) {
197
217
  // Server process exists but not responding, try to start fresh
198
218
  console.error('Server process exists but not responding, starting fresh...');
219
+ removeLockFileIfMatches(serverStatus);
199
220
  }
200
221
  }
201
222
 
@@ -39,7 +39,7 @@ function isServerRunning() {
39
39
  // Check if process exists
40
40
  try {
41
41
  process.kill(pid, 0); // Signal 0 checks existence without killing
42
- return { running: true, port };
42
+ return { running: true, pid, port };
43
43
  } catch (err) {
44
44
  // Process doesn't exist, remove stale lock file
45
45
  fs.unlinkSync(LOCK_FILE);
@@ -50,6 +50,26 @@ function isServerRunning() {
50
50
  }
51
51
  }
52
52
 
53
+ /**
54
+ * Remove the lock we just proved unhealthy, without deleting a newer sidecar's
55
+ * lock if another launcher won the race and replaced it first.
56
+ */
57
+ function removeLockFileIfMatches(staleLock) {
58
+ try {
59
+ if (!fs.existsSync(LOCK_FILE)) {
60
+ return false;
61
+ }
62
+ const currentLock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
63
+ if (currentLock.pid === staleLock.pid && currentLock.port === staleLock.port) {
64
+ fs.unlinkSync(LOCK_FILE);
65
+ return true;
66
+ }
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ return false;
71
+ }
72
+
53
73
  /**
54
74
  * Wait for server to respond to health check
55
75
  */
@@ -85,6 +105,44 @@ function waitForHealth(port, timeout = HEALTH_CHECK_TIMEOUT) {
85
105
  });
86
106
  }
87
107
 
108
+ /**
109
+ * Wait for the lock file to appear, then health-check the port it specifies.
110
+ *
111
+ * The sidecar may bind a non-default port when the default is occupied by an
112
+ * orphan. Polling DEFAULT_PORT would be fooled by the orphan, so we wait for
113
+ * the new sidecar to write its lock file and health-check THAT port.
114
+ */
115
+ function waitForLockAndHealth(timeout = HEALTH_CHECK_TIMEOUT) {
116
+ return new Promise((resolve, reject) => {
117
+ const startTime = Date.now();
118
+
119
+ const poll = () => {
120
+ if (Date.now() - startTime > timeout) {
121
+ return reject(new Error('Server health check timeout (lock file never appeared)'));
122
+ }
123
+
124
+ try {
125
+ if (fs.existsSync(LOCK_FILE)) {
126
+ const lockData = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
127
+ const port = lockData.port || DEFAULT_PORT;
128
+ // Lock file appeared — now health-check the actual port
129
+ const remaining = timeout - (Date.now() - startTime);
130
+ waitForHealth(port, Math.max(remaining, 1000))
131
+ .then(resolve)
132
+ .catch(reject);
133
+ return;
134
+ }
135
+ } catch (err) {
136
+ // Lock file partially written or unreadable — retry
137
+ }
138
+
139
+ setTimeout(poll, HEALTH_CHECK_INTERVAL);
140
+ };
141
+
142
+ poll();
143
+ });
144
+ }
145
+
88
146
  /**
89
147
  * Start the PMXT server
90
148
  */
@@ -136,8 +194,10 @@ async function startServer() {
136
194
  // Detach from parent process
137
195
  serverProcess.unref();
138
196
 
139
- // Wait for server to be ready
140
- await waitForHealth(DEFAULT_PORT);
197
+ // Wait for the sidecar to write its lock file, then health-check
198
+ // the actual port it bound (which may differ from DEFAULT_PORT if
199
+ // an orphan sidecar is occupying it).
200
+ await waitForLockAndHealth();
141
201
  }
142
202
 
143
203
  /**
@@ -156,6 +216,7 @@ async function main() {
156
216
  } catch (err) {
157
217
  // Server process exists but not responding, try to start fresh
158
218
  console.error('Server process exists but not responding, starting fresh...');
219
+ removeLockFileIfMatches(serverStatus);
159
220
  }
160
221
  }
161
222
 
@@ -627,8 +627,42 @@ export declare abstract class PredictionMarketExchange {
627
627
  * @returns Array of open orders
628
628
  */
629
629
  fetchOpenOrders(marketId?: string): Promise<Order[]>;
630
+ /**
631
+ * Fetch authenticated user trade history.
632
+ *
633
+ * @param params - Optional trade history filters
634
+ * @param params.outcomeId - Filter by outcome token ID
635
+ * @param params.marketId - Filter by market ID or ticker
636
+ * @param params.since - Start timestamp or ISO date
637
+ * @param params.until - End timestamp or ISO date
638
+ * @param params.limit - Maximum number of trades
639
+ * @param params.cursor - Pagination cursor
640
+ * @returns Array of user trades
641
+ */
630
642
  fetchMyTrades(params?: MyTradesParams): Promise<UserTrade[]>;
643
+ /**
644
+ * Fetch authenticated closed orders.
645
+ *
646
+ * @param params - Optional order history filters
647
+ * @param params.marketId - Filter by market ID or ticker
648
+ * @param params.since - Start timestamp or ISO date
649
+ * @param params.until - End timestamp or ISO date
650
+ * @param params.limit - Maximum number of orders
651
+ * @param params.cursor - Pagination cursor
652
+ * @returns Array of closed orders
653
+ */
631
654
  fetchClosedOrders(params?: OrderHistoryParams): Promise<Order[]>;
655
+ /**
656
+ * Fetch authenticated order history across open and closed orders.
657
+ *
658
+ * @param params - Optional order history filters
659
+ * @param params.marketId - Filter by market ID or ticker
660
+ * @param params.since - Start timestamp or ISO date
661
+ * @param params.until - End timestamp or ISO date
662
+ * @param params.limit - Maximum number of orders
663
+ * @param params.cursor - Pagination cursor
664
+ * @returns Array of orders
665
+ */
632
666
  fetchAllOrders(params?: OrderHistoryParams): Promise<Order[]>;
633
667
  /**
634
668
  * Fetch current user positions across all markets.
@@ -213,7 +213,13 @@ class PredictionMarketExchange {
213
213
  return limit !== undefined ? filtered.slice(start, start + limit) : filtered.slice(start);
214
214
  }
215
215
  const { limit, offset, ...venueParams } = fetchParams;
216
- const markets = await this.fetchMarketsImpl(Object.keys(venueParams).length > 0 ? venueParams : undefined);
216
+ const hasVenueParams = Object.keys(venueParams).length > 0;
217
+ const shouldForwardSimpleLimit = limit !== undefined && offset === undefined && !hasVenueParams;
218
+ const markets = await this.fetchMarketsImpl(shouldForwardSimpleLimit
219
+ ? { limit }
220
+ : hasVenueParams
221
+ ? venueParams
222
+ : undefined);
217
223
  const start = offset ?? 0;
218
224
  return limit !== undefined ? markets.slice(start, start + limit) : markets.slice(start);
219
225
  }
@@ -357,7 +363,9 @@ class PredictionMarketExchange {
357
363
  return limit !== undefined ? filtered.slice(start, start + limit) : filtered.slice(start);
358
364
  }
359
365
  const { limit, offset, ...venueParams } = fetchParams;
360
- const events = await this.fetchEventsImpl(venueParams);
366
+ const hasVenueParams = Object.keys(venueParams).length > 0;
367
+ const shouldForwardSimpleLimit = limit !== undefined && offset === undefined && !hasVenueParams;
368
+ const events = await this.fetchEventsImpl(shouldForwardSimpleLimit ? { limit } : venueParams);
361
369
  const start = offset ?? 0;
362
370
  return limit !== undefined ? events.slice(start, start + limit) : events.slice(start);
363
371
  }
@@ -607,12 +615,46 @@ class PredictionMarketExchange {
607
615
  async fetchOpenOrders(marketId) {
608
616
  throw new Error("Method fetchOpenOrders not implemented.");
609
617
  }
618
+ /**
619
+ * Fetch authenticated user trade history.
620
+ *
621
+ * @param params - Optional trade history filters
622
+ * @param params.outcomeId - Filter by outcome token ID
623
+ * @param params.marketId - Filter by market ID or ticker
624
+ * @param params.since - Start timestamp or ISO date
625
+ * @param params.until - End timestamp or ISO date
626
+ * @param params.limit - Maximum number of trades
627
+ * @param params.cursor - Pagination cursor
628
+ * @returns Array of user trades
629
+ */
610
630
  async fetchMyTrades(params) {
611
631
  throw new Error("Method fetchMyTrades not implemented.");
612
632
  }
633
+ /**
634
+ * Fetch authenticated closed orders.
635
+ *
636
+ * @param params - Optional order history filters
637
+ * @param params.marketId - Filter by market ID or ticker
638
+ * @param params.since - Start timestamp or ISO date
639
+ * @param params.until - End timestamp or ISO date
640
+ * @param params.limit - Maximum number of orders
641
+ * @param params.cursor - Pagination cursor
642
+ * @returns Array of closed orders
643
+ */
613
644
  async fetchClosedOrders(params) {
614
645
  throw new Error("Method fetchClosedOrders not implemented.");
615
646
  }
647
+ /**
648
+ * Fetch authenticated order history across open and closed orders.
649
+ *
650
+ * @param params - Optional order history filters
651
+ * @param params.marketId - Filter by market ID or ticker
652
+ * @param params.since - Start timestamp or ISO date
653
+ * @param params.until - End timestamp or ISO date
654
+ * @param params.limit - Maximum number of orders
655
+ * @param params.cursor - Pagination cursor
656
+ * @returns Array of orders
657
+ */
616
658
  async fetchAllOrders(params) {
617
659
  throw new Error("Method fetchAllOrders not implemented.");
618
660
  }
@@ -34,6 +34,10 @@ export interface HunchRawMarket {
34
34
  /** Present in the schema; absent on the bare list endpoint — optional. */
35
35
  volumeUsd?: number;
36
36
  totalBets?: number;
37
+ /** Trailing-24h pool inflow (USD); absent on older API builds — optional. */
38
+ volume24hUsd?: number;
39
+ /** Live binary YES/NO odds on the list item; null/absent for N-way markets. */
40
+ odds?: HunchRawBinaryOdds | null;
37
41
  targetMarketCapUsd: number | null;
38
42
  outcomes: HunchRawOutcome[] | null;
39
43
  headline?: string | null;
@@ -5,6 +5,8 @@ const utils_1 = require("./utils");
5
5
  const errors_1 = require("./errors");
6
6
  const AGENT_PREFIX = '/api/agent/v1';
7
7
  const DEFAULT_LIMIT = 200;
8
+ /** Safety backstop for cursor draining: 50 pages * 200 = 10k markets. */
9
+ const MAX_LIST_PAGES = 50;
8
10
  /**
9
11
  * HunchFetcher — raw GETs against the live agent API. Hunch reads are keyless,
10
12
  * so we hit the HTTP client directly (clearer + more robust than the implicit
@@ -28,6 +30,7 @@ class HunchFetcher {
28
30
  const one = await this.fetchRawMarketById(params.slug);
29
31
  return one ? [one] : [];
30
32
  }
33
+ const explicitLimit = typeof params?.limit === 'number';
31
34
  const query = {
32
35
  limit: params?.limit ?? DEFAULT_LIMIT,
33
36
  };
@@ -36,12 +39,22 @@ class HunchFetcher {
36
39
  query.status = status;
37
40
  if (params?.query)
38
41
  query.token = params.query;
39
- const res = await this.ctx.http.get(`${this.baseUrl}${AGENT_PREFIX}/markets`, {
40
- params: query,
41
- headers: this.ctx.getHeaders(),
42
- });
43
- const markets = res.data?.markets ?? [];
44
- return markets;
42
+ // A catalog crawl (no explicit limit) follows `nextCursor` to drain
43
+ // the whole list; an explicit limit fetches just that one page.
44
+ // MAX_LIST_PAGES is a safety backstop against a runaway cursor loop.
45
+ const all = [];
46
+ let cursor;
47
+ let pages = 0;
48
+ do {
49
+ const res = await this.ctx.http.get(`${this.baseUrl}${AGENT_PREFIX}/markets`, {
50
+ params: cursor ? { ...query, cursor } : query,
51
+ headers: this.ctx.getHeaders(),
52
+ });
53
+ all.push(...(res.data?.markets ?? []));
54
+ cursor = typeof res.data?.nextCursor === 'string' ? res.data.nextCursor : undefined;
55
+ pages += 1;
56
+ } while (!explicitLimit && cursor && pages < MAX_LIST_PAGES);
57
+ return all;
45
58
  }
46
59
  catch (error) {
47
60
  throw errors_1.hunchErrorMapper.mapError(error);
@@ -45,6 +45,14 @@ export declare function parseHunchSide(outcomeId: string): {
45
45
  marketId: string;
46
46
  side: string;
47
47
  };
48
+ /**
49
+ * Map a Hunch native category to a pmxt top-level category. Hunch is
50
+ * crypto-native, so every token/price/on-chain subtype rolls up to "Crypto";
51
+ * only the manual "event" markets (fights/debates) map elsewhere.
52
+ */
53
+ export declare function mapHunchCategory(rawCategory: string | undefined): string;
54
+ /** Tags = top category + a human subtype label + the token (deduped, non-empty). */
55
+ export declare function hunchMarketTags(raw: HunchRawMarket): string[];
48
56
  /**
49
57
  * Shared market normalizer used by both the live fetch path and the (rare)
50
58
  * direct-mapping helper. Pulls a Hunch market ref into a {@link UnifiedMarket}.
@@ -5,6 +5,8 @@ exports.mapHunchStatus = mapHunchStatus;
5
5
  exports.mapStatusToHunch = mapStatusToHunch;
6
6
  exports.buildOutcomeId = buildOutcomeId;
7
7
  exports.parseHunchSide = parseHunchSide;
8
+ exports.mapHunchCategory = mapHunchCategory;
9
+ exports.hunchMarketTags = hunchMarketTags;
8
10
  exports.mapHunchMarketToUnified = mapHunchMarketToUnified;
9
11
  const market_utils_1 = require("../../utils/market-utils");
10
12
  const metadata_1 = require("../../utils/metadata");
@@ -102,6 +104,55 @@ function parseHunchSide(outcomeId) {
102
104
  side: outcomeId.slice(idx + 1),
103
105
  };
104
106
  }
107
+ // ---------------------------------------------------------------------------
108
+ // Category + tags — map Hunch's fine-grained native taxonomy onto pmxt's
109
+ // top-level categories (Crypto / Culture / …) + granular tags, so Hunch
110
+ // markets answer `?category=` filters and match with higher confidence.
111
+ // ---------------------------------------------------------------------------
112
+ /** Hunch native categories that are NOT crypto (manual-resolution markets). */
113
+ const HUNCH_TOP_CATEGORY = {
114
+ event: 'Culture',
115
+ };
116
+ /**
117
+ * Map a Hunch native category to a pmxt top-level category. Hunch is
118
+ * crypto-native, so every token/price/on-chain subtype rolls up to "Crypto";
119
+ * only the manual "event" markets (fights/debates) map elsewhere.
120
+ */
121
+ function mapHunchCategory(rawCategory) {
122
+ return HUNCH_TOP_CATEGORY[rawCategory ?? ''] ?? 'Crypto';
123
+ }
124
+ /** Human-readable granular label per Hunch native category (for tags). */
125
+ const HUNCH_SUBTYPE_LABEL = {
126
+ market_cap: 'Market Cap',
127
+ token_mcap_range: 'Market Cap',
128
+ token_mcap_flip: 'Market Cap',
129
+ token_mcap_close: 'Market Cap',
130
+ token_basket_mcap: 'Market Cap',
131
+ price_direction: 'Price',
132
+ token_price_range: 'Price',
133
+ token_return: 'Returns',
134
+ token_rank_milestone: 'Ranking',
135
+ chain_volume: 'On-chain Volume',
136
+ chain_throughput: 'Throughput',
137
+ chain_stablecoins: 'Stablecoins',
138
+ launchpad_volume: 'Launchpad',
139
+ dune_metric: 'On-chain',
140
+ volume_eta: 'Volume',
141
+ event: 'Event',
142
+ };
143
+ function titleCaseSlug(s) {
144
+ return s
145
+ .split(/[_\s-]+/)
146
+ .filter(Boolean)
147
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
148
+ .join(' ');
149
+ }
150
+ /** Tags = top category + a human subtype label + the token (deduped, non-empty). */
151
+ function hunchMarketTags(raw) {
152
+ const top = mapHunchCategory(raw.category);
153
+ const label = HUNCH_SUBTYPE_LABEL[raw.category] ?? (raw.category ? titleCaseSlug(raw.category) : '');
154
+ return [...new Set([top, label, raw.tokenSymbol].filter((t) => Boolean(t)))];
155
+ }
105
156
  /**
106
157
  * Shared market normalizer used by both the live fetch path and the (rare)
107
158
  * direct-mapping helper. Pulls a Hunch market ref into a {@link UnifiedMarket}.
@@ -119,6 +170,9 @@ function mapHunchMarketToUnified(raw, odds, ladder) {
119
170
  if (!raw || !raw.id)
120
171
  return null;
121
172
  const marketId = raw.id;
173
+ // Explicit odds (detail/quote read) win; else fall back to the live odds the
174
+ // list item now carries — so a bare list-crawl prices binary markets too.
175
+ const effectiveOdds = odds ?? raw.odds ?? null;
122
176
  let outcomes;
123
177
  if (Array.isArray(raw.outcomes) && raw.outcomes.length > 0) {
124
178
  // N-way parimutuel ladder / date-window market.
@@ -148,9 +202,9 @@ function mapHunchMarketToUnified(raw, odds, ladder) {
148
202
  }
149
203
  else {
150
204
  // Binary YES/NO market.
151
- const yesPrice = odds && typeof odds.yesPriceCents === 'number' ? odds.yesPriceCents / 100 : 0;
152
- const noPrice = odds && typeof odds.noPriceCents === 'number'
153
- ? odds.noPriceCents / 100
205
+ const yesPrice = effectiveOdds && typeof effectiveOdds.yesPriceCents === 'number' ? effectiveOdds.yesPriceCents / 100 : 0;
206
+ const noPrice = effectiveOdds && typeof effectiveOdds.noPriceCents === 'number'
207
+ ? effectiveOdds.noPriceCents / 100
154
208
  : yesPrice > 0
155
209
  ? 1 - yesPrice
156
210
  : 0;
@@ -166,13 +220,13 @@ function mapHunchMarketToUnified(raw, odds, ladder) {
166
220
  slug: raw.slug,
167
221
  outcomes,
168
222
  resolutionDate: raw.deadlineAt ? new Date(raw.deadlineAt) : undefined,
169
- // Hunch is parimutuel and reports no 24h volume split surface 0.
170
- volume24h: 0,
223
+ // Trailing-24h pool inflow off the list item (0 when absent / no trades).
224
+ volume24h: typeof raw.volume24hUsd === 'number' ? raw.volume24hUsd : 0,
171
225
  volume: typeof raw.volumeUsd === 'number' ? raw.volumeUsd : undefined,
172
226
  liquidity: Number(raw.virtualLiquidityUsd || 0),
173
227
  url: raw.links?.app || `${exports.DEFAULT_BASE_URL}/markets/${raw.slug || marketId}`,
174
- category: raw.category,
175
- tags: raw.tokenSymbol ? [raw.tokenSymbol] : [],
228
+ category: mapHunchCategory(raw.category),
229
+ tags: hunchMarketTags(raw),
176
230
  status: mapHunchStatus(raw.status),
177
231
  sourceMetadata: (0, metadata_1.buildSourceMetadata)(raw, exports.HUNCH_PROMOTED_MARKET_KEYS),
178
232
  };
@@ -175,7 +175,7 @@ class HyperliquidNormalizer {
175
175
  description: outcome.description,
176
176
  slug: `hl-${outcomeId}`,
177
177
  outcomes,
178
- resolutionDate: expiryDate ?? new Date(0),
178
+ resolutionDate: expiryDate,
179
179
  volume24h: raw.volume24h ?? 0,
180
180
  liquidity: 0,
181
181
  url: buildMarketUrl(outcomeId),
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/kalshi/Kalshi.yaml
3
- * Generated at: 2026-06-23T06:11:27.017Z
3
+ * Generated at: 2026-06-24T09:17:49.567Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const kalshiApiSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.kalshiApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/kalshi/Kalshi.yaml
6
- * Generated at: 2026-06-23T06:11:27.017Z
6
+ * Generated at: 2026-06-24T09:17:49.567Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.kalshiApiSpec = {
@@ -64,6 +64,19 @@ class KalshiFetcher {
64
64
  return this.fetchAllWithSeriesTicker(params.series);
65
65
  }
66
66
  const status = params?.status || 'active';
67
+ const hasBoundedDefaultRead = status === 'active' &&
68
+ (params.limit !== undefined || params.cursor !== undefined) &&
69
+ !params.query &&
70
+ params.offset === undefined &&
71
+ params.sort === undefined &&
72
+ params.searchIn === undefined &&
73
+ params.category === undefined &&
74
+ params.tags === undefined &&
75
+ params.filter === undefined;
76
+ if (hasBoundedDefaultRead) {
77
+ const page = await this.fetchRawEventPage(params);
78
+ return page.events;
79
+ }
67
80
  if (status === 'all') {
68
81
  const openEvents = await this.fetchAllWithStatus('open');
69
82
  const closedEvents = await this.fetchAllWithStatus('closed');
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/limitless/Limitless.yaml
3
- * Generated at: 2026-06-23T06:11:27.061Z
3
+ * Generated at: 2026-06-24T09:17:49.606Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const limitlessApiSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.limitlessApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/limitless/Limitless.yaml
6
- * Generated at: 2026-06-23T06:11:27.061Z
6
+ * Generated at: 2026-06-24T09:17:49.606Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.limitlessApiSpec = {
@@ -100,7 +100,10 @@ class LimitlessExchange extends BaseExchange_1.PredictionMarketExchange {
100
100
  callApi: this.callApi.bind(this),
101
101
  getHeaders: () => this.getHeaders(),
102
102
  };
103
- const limitlessBaseUrl = this.auth?.host || credentials?.baseUrl || utils_1.DEFAULT_LIMITLESS_API_URL;
103
+ const limitlessBaseUrl = this.auth?.host ||
104
+ credentials?.baseUrl ||
105
+ process.env.LIMITLESS_BASE_URL ||
106
+ utils_1.DEFAULT_LIMITLESS_API_URL;
104
107
  this.fetcher = new fetcher_1.LimitlessFetcher(ctx, this.http, this.auth?.getApiKey(), limitlessBaseUrl);
105
108
  this.normalizer = new normalizer_1.LimitlessNormalizer();
106
109
  }
@@ -58,7 +58,8 @@ class MetaculusExchange extends BaseExchange_1.PredictionMarketExchange {
58
58
  this.apiToken = credentials?.apiToken;
59
59
  // Rate-limit conservatively; authenticated users get higher Metaculus quotas
60
60
  this.rateLimit = 500;
61
- this.baseUrl = credentials?.baseUrl || utils_1.DEFAULT_BASE_URL;
61
+ this.baseUrl =
62
+ credentials?.baseUrl || process.env.METACULUS_BASE_URL || utils_1.DEFAULT_BASE_URL;
62
63
  const descriptor = (0, openapi_1.parseOpenApiSpec)(api_1.metaculusApiSpec, this.baseUrl);
63
64
  this.defineImplicitApi(descriptor);
64
65
  }
@@ -43,7 +43,7 @@ export declare class MockExchange extends PredictionMarketExchange {
43
43
  fillOrder(orderId: string, amount?: number): Promise<Order>;
44
44
  cancelOrder(orderId: string): Promise<Order>;
45
45
  fetchOrder(orderId: string): Promise<Order>;
46
- fetchOpenOrders(_marketId?: string): Promise<Order[]>;
46
+ fetchOpenOrders(marketId?: string): Promise<Order[]>;
47
47
  fetchMyTrades(_params?: {
48
48
  outcomeId?: string;
49
49
  marketId?: string;