pmxt-core 2.35.30 → 2.35.32

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.
@@ -217,6 +217,8 @@ export interface ExchangeHas {
217
217
  unwatchAddress: ExchangeCapability;
218
218
  /** Whether this exchange supports streaming order book updates. */
219
219
  watchOrderBook: ExchangeCapability;
220
+ /** Whether this exchange supports batch-subscribing to multiple order book streams. */
221
+ watchOrderBooks: ExchangeCapability;
220
222
  /** Whether this exchange supports unsubscribing from an order book stream. */
221
223
  unwatchOrderBook: ExchangeCapability;
222
224
  /** Whether this exchange supports streaming trade updates. */
@@ -574,6 +576,17 @@ export declare abstract class PredictionMarketExchange {
574
576
  * @returns Promise that resolves with the current orderbook state
575
577
  */
576
578
  watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
579
+ /**
580
+ * Watch multiple order books simultaneously via WebSocket.
581
+ * Returns a promise that resolves with a record of order book snapshots keyed by ID.
582
+ * Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
583
+ * for all tickers; others fall back to individual watchOrderBook calls.
584
+ *
585
+ * @param ids - Array of Outcome IDs to watch
586
+ * @param limit - Optional limit for orderbook depth
587
+ * @returns Promise that resolves with order books keyed by ID
588
+ */
589
+ watchOrderBooks(ids: string[], limit?: number): Promise<Record<string, OrderBook>>;
577
590
  /**
578
591
  * Unsubscribe from a previously watched order book stream.
579
592
  *
@@ -764,6 +764,30 @@ class PredictionMarketExchange {
764
764
  async watchOrderBook(id, limit) {
765
765
  throw new Error(`watchOrderBook() is not supported by ${this.name}`);
766
766
  }
767
+ /**
768
+ * Watch multiple order books simultaneously via WebSocket.
769
+ * Returns a promise that resolves with a record of order book snapshots keyed by ID.
770
+ * Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
771
+ * for all tickers; others fall back to individual watchOrderBook calls.
772
+ *
773
+ * @param ids - Array of Outcome IDs to watch
774
+ * @param limit - Optional limit for orderbook depth
775
+ * @returns Promise that resolves with order books keyed by ID
776
+ */
777
+ async watchOrderBooks(ids, limit) {
778
+ // Default implementation: subscribe to each ID individually.
779
+ // Exchanges with native batch support (e.g. Kalshi) override this
780
+ // to send a single subscribe message for all tickers.
781
+ const entries = await Promise.all(ids.map(async (id) => {
782
+ const book = await this.watchOrderBook(id, limit);
783
+ return [id, book];
784
+ }));
785
+ const result = {};
786
+ for (const [id, book] of entries) {
787
+ result[id] = book;
788
+ }
789
+ return result;
790
+ }
767
791
  /**
768
792
  * Unsubscribe from a previously watched order book stream.
769
793
  *
@@ -1033,7 +1057,7 @@ class PredictionMarketExchange {
1033
1057
  'fetchMarkets', 'fetchEvents', 'fetchOHLCV', 'fetchOrderBook',
1034
1058
  'fetchTrades', 'createOrder', 'cancelOrder', 'fetchOrder',
1035
1059
  'fetchOpenOrders', 'fetchPositions', 'fetchBalance',
1036
- 'watchAddress', 'unwatchAddress', 'watchOrderBook',
1060
+ 'watchAddress', 'unwatchAddress', 'watchOrderBook', 'watchOrderBooks',
1037
1061
  'unwatchOrderBook', 'watchTrades', 'fetchMyTrades',
1038
1062
  'fetchClosedOrders', 'fetchAllOrders', 'buildOrder', 'submitOrder',
1039
1063
  'fetchMarketMatches', 'fetchMatches', 'fetchEventMatches', 'compareMarketPrices',
@@ -1046,7 +1070,7 @@ class PredictionMarketExchange {
1046
1070
  fetchOrderBook: true, fetchTrades: true, createOrder: true,
1047
1071
  cancelOrder: true, fetchOrder: true, fetchOpenOrders: true,
1048
1072
  fetchPositions: true, fetchBalance: true, watchAddress: true,
1049
- unwatchAddress: true, watchOrderBook: true, unwatchOrderBook: true,
1073
+ unwatchAddress: true, watchOrderBook: true, watchOrderBooks: true, unwatchOrderBook: true,
1050
1074
  watchTrades: true, fetchMyTrades: true, fetchClosedOrders: true,
1051
1075
  fetchAllOrders: true, buildOrder: true, submitOrder: true,
1052
1076
  fetchMarketMatches: true, fetchMatches: true, fetchEventMatches: true, compareMarketPrices: true,
@@ -1061,6 +1085,7 @@ class PredictionMarketExchange {
1061
1085
  static _capabilityDelegates = {
1062
1086
  fetchMarkets: 'fetchMarketsImpl',
1063
1087
  fetchEvents: 'fetchEventsImpl',
1088
+ watchOrderBooks: 'watchOrderBook',
1064
1089
  fetchMatches: 'fetchMarketMatches',
1065
1090
  fetchHedges: 'fetchRelatedMarkets',
1066
1091
  fetchMatchedPrices: 'fetchMatchedMarkets',
@@ -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-04-30T15:51:55.936Z
3
+ * Generated at: 2026-05-01T12:04:30.287Z
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-04-30T15:51:55.936Z
6
+ * Generated at: 2026-05-01T12:04:30.287Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.kalshiApiSpec = {
@@ -39,6 +39,7 @@ export declare class KalshiExchange extends PredictionMarketExchange {
39
39
  fetchOpenOrders(marketId?: string): Promise<Order[]>;
40
40
  private ws?;
41
41
  watchOrderBook(id: string, limit?: number): Promise<OrderBook>;
42
+ watchOrderBooks(ids: string[], limit?: number): Promise<Record<string, OrderBook>>;
42
43
  watchTrades(id: string, address?: string, since?: number, limit?: number): Promise<Trade[]>;
43
44
  close(): Promise<void>;
44
45
  }
@@ -263,6 +263,18 @@ class KalshiExchange extends BaseExchange_1.PredictionMarketExchange {
263
263
  const marketTicker = id.replace(/-NO$/, "");
264
264
  return this.ws.watchOrderBook(marketTicker);
265
265
  }
266
+ async watchOrderBooks(ids, limit) {
267
+ const auth = this.ensureAuth();
268
+ if (!this.ws) {
269
+ const wsConfigWithUrl = {
270
+ ...this.wsConfig,
271
+ wsUrl: this.wsConfig?.wsUrl || this.config.wsUrl,
272
+ };
273
+ this.ws = new websocket_1.KalshiWebSocket(auth, wsConfigWithUrl);
274
+ }
275
+ const marketTickers = ids.map((id) => id.replace(/-NO$/, ""));
276
+ return this.ws.watchOrderBooks(marketTickers);
277
+ }
266
278
  async watchTrades(id, address, since, limit) {
267
279
  const auth = this.ensureAuth();
268
280
  if (!this.ws) {
@@ -38,6 +38,7 @@ export declare class KalshiWebSocket {
38
38
  private handleTrade;
39
39
  private resolveOrderBook;
40
40
  watchOrderBook(ticker: string): Promise<OrderBook>;
41
+ watchOrderBooks(tickers: string[]): Promise<Record<string, OrderBook>>;
41
42
  watchTrades(ticker: string): Promise<Trade[]>;
42
43
  close(): Promise<void>;
43
44
  }
@@ -324,7 +324,7 @@ class KalshiWebSocket {
324
324
  // Subscribe if not already subscribed
325
325
  if (!this.subscribedOrderBookTickers.has(ticker)) {
326
326
  this.subscribedOrderBookTickers.add(ticker);
327
- this.subscribeToOrderbook(Array.from(this.subscribedOrderBookTickers));
327
+ this.subscribeToOrderbook([ticker]);
328
328
  }
329
329
  // Return a promise that resolves on the next orderbook update
330
330
  return new Promise((resolve, reject) => {
@@ -334,6 +334,40 @@ class KalshiWebSocket {
334
334
  this.orderBookResolvers.get(ticker).push({ resolve, reject });
335
335
  });
336
336
  }
337
+ async watchOrderBooks(tickers) {
338
+ // Ensure connection
339
+ if (!this.isConnected) {
340
+ await this.connect();
341
+ }
342
+ // Determine which tickers are actually new (not yet subscribed)
343
+ const newTickers = tickers.filter((t) => !this.subscribedOrderBookTickers.has(t));
344
+ // Add all new tickers to the subscription set
345
+ for (const t of newTickers) {
346
+ this.subscribedOrderBookTickers.add(t);
347
+ }
348
+ // Send ONE subscribe message with only the new tickers
349
+ if (newTickers.length > 0) {
350
+ this.subscribeToOrderbook(newTickers);
351
+ }
352
+ // Wait for all tickers to receive at least one snapshot/update
353
+ const entries = await Promise.all(tickers.map((ticker) => new Promise((resolve, reject) => {
354
+ if (!this.orderBookResolvers.has(ticker)) {
355
+ this.orderBookResolvers.set(ticker, []);
356
+ }
357
+ this.orderBookResolvers.get(ticker).push({
358
+ resolve: (book) => {
359
+ // Unwrap PromiseLike if needed, but in practice it is always OrderBook
360
+ Promise.resolve(book).then((b) => resolve([ticker, b]));
361
+ },
362
+ reject,
363
+ });
364
+ })));
365
+ const result = {};
366
+ for (const [ticker, book] of entries) {
367
+ result[ticker] = book;
368
+ }
369
+ return result;
370
+ }
337
371
  async watchTrades(ticker) {
338
372
  // Ensure connection
339
373
  if (!this.isConnected) {
@@ -342,7 +376,7 @@ class KalshiWebSocket {
342
376
  // Subscribe if not already subscribed
343
377
  if (!this.subscribedTradeTickers.has(ticker)) {
344
378
  this.subscribedTradeTickers.add(ticker);
345
- this.subscribeToTrades(Array.from(this.subscribedTradeTickers));
379
+ this.subscribeToTrades([ticker]);
346
380
  }
347
381
  // Return a promise that resolves on the next trade
348
382
  return new Promise((resolve, reject) => {
@@ -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-04-30T15:51:55.979Z
3
+ * Generated at: 2026-05-01T12:04:30.324Z
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-04-30T15:51:55.979Z
6
+ * Generated at: 2026-05-01T12:04:30.324Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.limitlessApiSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/myriad/myriad.yaml
3
- * Generated at: 2026-04-30T15:51:55.991Z
3
+ * Generated at: 2026-05-01T12:04:30.336Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const myriadApiSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.myriadApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/myriad/myriad.yaml
6
- * Generated at: 2026-04-30T15:51:55.991Z
6
+ * Generated at: 2026-05-01T12:04:30.336Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.myriadApiSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/opinion/opinion-openapi.yaml
3
- * Generated at: 2026-04-30T15:51:55.994Z
3
+ * Generated at: 2026-05-01T12:04:30.340Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const opinionApiSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.opinionApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/opinion/opinion-openapi.yaml
6
- * Generated at: 2026-04-30T15:51:55.994Z
6
+ * Generated at: 2026-05-01T12:04:30.340Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.opinionApiSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketClobAPI.yaml
3
- * Generated at: 2026-04-30T15:51:55.945Z
3
+ * Generated at: 2026-05-01T12:04:30.294Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const polymarketClobSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.polymarketClobSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketClobAPI.yaml
6
- * Generated at: 2026-04-30T15:51:55.945Z
6
+ * Generated at: 2026-05-01T12:04:30.294Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.polymarketClobSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/Polymarket_Data_API.yaml
3
- * Generated at: 2026-04-30T15:51:55.961Z
3
+ * Generated at: 2026-05-01T12:04:30.307Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const polymarketDataSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.polymarketDataSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/Polymarket_Data_API.yaml
6
- * Generated at: 2026-04-30T15:51:55.961Z
6
+ * Generated at: 2026-05-01T12:04:30.307Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.polymarketDataSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketGammaAPI.yaml
3
- * Generated at: 2026-04-30T15:51:55.956Z
3
+ * Generated at: 2026-05-01T12:04:30.304Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const polymarketGammaSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.polymarketGammaSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/polymarket/PolymarketGammaAPI.yaml
6
- * Generated at: 2026-04-30T15:51:55.956Z
6
+ * Generated at: 2026-05-01T12:04:30.304Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.polymarketGammaSpec = {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/probable/probable.yaml
3
- * Generated at: 2026-04-30T15:51:55.985Z
3
+ * Generated at: 2026-05-01T12:04:30.329Z
4
4
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
5
5
  */
6
6
  export declare const probableApiSpec: {
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.probableApiSpec = void 0;
4
4
  /**
5
5
  * Auto-generated from /home/runner/work/pmxt/pmxt/core/specs/probable/probable.yaml
6
- * Generated at: 2026-04-30T15:51:55.985Z
6
+ * Generated at: 2026-05-01T12:04:30.329Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.probableApiSpec = {
@@ -1,4 +1,5 @@
1
1
  import { Express } from "express";
2
+ import { createWebSocketHandler, CreateWebSocketHandlerOptions } from "./ws-handler";
2
3
  /**
3
4
  * Options accepted by {@link createApp}.
4
5
  */
@@ -54,5 +55,10 @@ export declare function createApp(options?: CreateAppOptions): Express;
54
55
  * Start the PMXT sidecar server on the given port with the built-in
55
56
  * access-token auth middleware enabled. Returns the underlying
56
57
  * {@link http.Server} once it is listening.
58
+ *
59
+ * Automatically attaches a WebSocket endpoint at `/ws` for streaming
60
+ * methods (watchOrderBook, watchOrderBooks, watchTrades).
57
61
  */
58
62
  export declare function startServer(port: number, accessToken: string): Promise<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>>;
63
+ export { createWebSocketHandler };
64
+ export type { CreateWebSocketHandlerOptions };
@@ -3,24 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createWebSocketHandler = void 0;
6
7
  exports.createApp = createApp;
7
8
  exports.startServer = startServer;
8
9
  const express_1 = __importDefault(require("express"));
9
10
  const cors_1 = __importDefault(require("cors"));
10
11
  const fs_1 = __importDefault(require("fs"));
11
12
  const path_1 = __importDefault(require("path"));
12
- const polymarket_1 = require("../exchanges/polymarket");
13
- const limitless_1 = require("../exchanges/limitless");
14
- const kalshi_1 = require("../exchanges/kalshi");
15
- const kalshi_demo_1 = require("../exchanges/kalshi-demo");
16
- const probable_1 = require("../exchanges/probable");
17
- const baozi_1 = require("../exchanges/baozi");
18
- const myriad_1 = require("../exchanges/myriad");
19
- const opinion_1 = require("../exchanges/opinion");
20
- const metaculus_1 = require("../exchanges/metaculus");
21
- const smarkets_1 = require("../exchanges/smarkets");
22
- const polymarket_us_1 = require("../exchanges/polymarket_us");
23
- const router_1 = require("../router");
13
+ const ws_handler_1 = require("./ws-handler");
14
+ Object.defineProperty(exports, "createWebSocketHandler", { enumerable: true, get: function () { return ws_handler_1.createWebSocketHandler; } });
15
+ const exchange_factory_1 = require("./exchange-factory");
24
16
  const errors_1 = require("../errors");
25
17
  function loadMethodVerbs() {
26
18
  const candidates = [
@@ -220,17 +212,17 @@ function createApp(options = {}) {
220
212
  // calls — not a server-side env var. Each request may carry a
221
213
  // different key, so Router is never cached as a singleton.
222
214
  const bearer = req.headers.authorization?.replace(/^Bearer\s+/i, "") || "";
223
- exchange = createExchange(exchangeName, undefined, bearer);
215
+ exchange = (0, exchange_factory_1.createExchange)(exchangeName, undefined, bearer);
224
216
  }
225
217
  else if (credentials &&
226
218
  (credentials.privateKey ||
227
219
  credentials.apiKey ||
228
220
  credentials.apiToken)) {
229
- exchange = createExchange(exchangeName, credentials);
221
+ exchange = (0, exchange_factory_1.createExchange)(exchangeName, credentials);
230
222
  }
231
223
  else {
232
224
  if (!defaultExchanges[exchangeName]) {
233
- defaultExchanges[exchangeName] = createExchange(exchangeName);
225
+ defaultExchanges[exchangeName] = (0, exchange_factory_1.createExchange)(exchangeName);
234
226
  }
235
227
  exchange = defaultExchanges[exchangeName];
236
228
  }
@@ -336,93 +328,15 @@ function createApp(options = {}) {
336
328
  * Start the PMXT sidecar server on the given port with the built-in
337
329
  * access-token auth middleware enabled. Returns the underlying
338
330
  * {@link http.Server} once it is listening.
331
+ *
332
+ * Automatically attaches a WebSocket endpoint at `/ws` for streaming
333
+ * methods (watchOrderBook, watchOrderBooks, watchTrades).
339
334
  */
340
335
  async function startServer(port, accessToken) {
341
336
  const app = createApp({ accessToken });
342
- return app.listen(port, "127.0.0.1");
343
- }
344
- function createExchange(name, credentials, bearerToken) {
345
- switch (name) {
346
- case "polymarket":
347
- return new polymarket_1.PolymarketExchange({
348
- privateKey: credentials?.privateKey ||
349
- process.env.POLYMARKET_PK ||
350
- process.env.POLYMARKET_PRIVATE_KEY,
351
- apiKey: credentials?.apiKey || process.env.POLYMARKET_API_KEY,
352
- apiSecret: credentials?.apiSecret || process.env.POLYMARKET_API_SECRET,
353
- passphrase: credentials?.passphrase || process.env.POLYMARKET_PASSPHRASE,
354
- funderAddress: credentials?.funderAddress ||
355
- process.env.POLYMARKET_FUNDER_ADDRESS ||
356
- process.env.POLYMARKET_PROXY_ADDRESS,
357
- signatureType: credentials?.signatureType ||
358
- process.env.POLYMARKET_SIGNATURE_TYPE,
359
- });
360
- case "limitless":
361
- return new limitless_1.LimitlessExchange({
362
- privateKey: credentials?.privateKey ||
363
- process.env.LIMITLESS_PK ||
364
- process.env.LIMITLESS_PRIVATE_KEY,
365
- apiKey: credentials?.apiKey || process.env.LIMITLESS_API_KEY,
366
- apiSecret: credentials?.apiSecret || process.env.LIMITLESS_API_SECRET,
367
- passphrase: credentials?.passphrase || process.env.LIMITLESS_PASSPHRASE,
368
- });
369
- case "kalshi":
370
- return new kalshi_1.KalshiExchange({
371
- credentials: {
372
- apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
373
- privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
374
- },
375
- });
376
- case "kalshi-demo":
377
- return new kalshi_demo_1.KalshiDemoExchange({
378
- credentials: {
379
- apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
380
- privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
381
- },
382
- });
383
- case "probable":
384
- return new probable_1.ProbableExchange({
385
- apiKey: credentials?.apiKey || process.env.PROBABLE_API_KEY,
386
- apiSecret: credentials?.apiSecret || process.env.PROBABLE_API_SECRET,
387
- passphrase: credentials?.passphrase || process.env.PROBABLE_PASSPHRASE,
388
- privateKey: credentials?.privateKey || process.env.PROBABLE_PRIVATE_KEY,
389
- });
390
- case "baozi":
391
- return new baozi_1.BaoziExchange({
392
- privateKey: credentials?.privateKey || process.env.BAOZI_PRIVATE_KEY,
393
- });
394
- case "myriad":
395
- return new myriad_1.MyriadExchange({
396
- apiKey: credentials?.apiKey ||
397
- process.env.MYRIAD_API_KEY ||
398
- process.env.MYRIAD_PROD,
399
- privateKey: credentials?.privateKey || process.env.MYRIAD_WALLET_ADDRESS,
400
- });
401
- case "opinion":
402
- return new opinion_1.OpinionExchange({
403
- apiKey: credentials?.apiKey || process.env.OPINION_API_KEY,
404
- privateKey: credentials?.privateKey || process.env.OPINION_PRIVATE_KEY,
405
- funderAddress: credentials?.funderAddress,
406
- });
407
- case "metaculus":
408
- return new metaculus_1.MetaculusExchange({
409
- apiToken: credentials?.apiToken || process.env.METACULUS_API_TOKEN,
410
- });
411
- case "smarkets":
412
- return new smarkets_1.SmarketsExchange({
413
- apiKey: credentials?.apiKey || process.env.SMARKETS_EMAIL,
414
- privateKey: credentials?.privateKey || process.env.SMARKETS_PASSWORD,
415
- });
416
- case "polymarket_us":
417
- return new polymarket_us_1.PolymarketUSExchange({
418
- apiKey: credentials?.apiKey || process.env.POLYMARKET_US_KEY_ID,
419
- privateKey: credentials?.privateKey || process.env.POLYMARKET_US_SECRET_KEY,
420
- });
421
- case "router":
422
- return new router_1.Router({
423
- apiKey: bearerToken,
424
- });
425
- default:
426
- throw new Error(`Unknown exchange: ${name}`);
427
- }
337
+ const server = app.listen(port, "127.0.0.1");
338
+ // Attach WebSocket handler for streaming subscriptions
339
+ const wsHandler = (0, ws_handler_1.createWebSocketHandler)({ accessToken });
340
+ wsHandler.attach(server);
341
+ return server;
428
342
  }
@@ -0,0 +1,2 @@
1
+ import { ExchangeCredentials } from "../BaseExchange";
2
+ export declare function createExchange(name: string, credentials?: ExchangeCredentials, bearerToken?: string): unknown;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExchange = createExchange;
4
+ const polymarket_1 = require("../exchanges/polymarket");
5
+ const limitless_1 = require("../exchanges/limitless");
6
+ const kalshi_1 = require("../exchanges/kalshi");
7
+ const kalshi_demo_1 = require("../exchanges/kalshi-demo");
8
+ const probable_1 = require("../exchanges/probable");
9
+ const baozi_1 = require("../exchanges/baozi");
10
+ const myriad_1 = require("../exchanges/myriad");
11
+ const opinion_1 = require("../exchanges/opinion");
12
+ const metaculus_1 = require("../exchanges/metaculus");
13
+ const smarkets_1 = require("../exchanges/smarkets");
14
+ const polymarket_us_1 = require("../exchanges/polymarket_us");
15
+ const router_1 = require("../router");
16
+ function createExchange(name, credentials, bearerToken) {
17
+ switch (name) {
18
+ case "polymarket":
19
+ return new polymarket_1.PolymarketExchange({
20
+ privateKey: credentials?.privateKey ||
21
+ process.env.POLYMARKET_PK ||
22
+ process.env.POLYMARKET_PRIVATE_KEY,
23
+ apiKey: credentials?.apiKey || process.env.POLYMARKET_API_KEY,
24
+ apiSecret: credentials?.apiSecret || process.env.POLYMARKET_API_SECRET,
25
+ passphrase: credentials?.passphrase || process.env.POLYMARKET_PASSPHRASE,
26
+ funderAddress: credentials?.funderAddress ||
27
+ process.env.POLYMARKET_FUNDER_ADDRESS ||
28
+ process.env.POLYMARKET_PROXY_ADDRESS,
29
+ signatureType: credentials?.signatureType ||
30
+ process.env.POLYMARKET_SIGNATURE_TYPE,
31
+ });
32
+ case "limitless":
33
+ return new limitless_1.LimitlessExchange({
34
+ privateKey: credentials?.privateKey ||
35
+ process.env.LIMITLESS_PK ||
36
+ process.env.LIMITLESS_PRIVATE_KEY,
37
+ apiKey: credentials?.apiKey || process.env.LIMITLESS_API_KEY,
38
+ apiSecret: credentials?.apiSecret || process.env.LIMITLESS_API_SECRET,
39
+ passphrase: credentials?.passphrase || process.env.LIMITLESS_PASSPHRASE,
40
+ });
41
+ case "kalshi":
42
+ return new kalshi_1.KalshiExchange({
43
+ credentials: {
44
+ apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
45
+ privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
46
+ },
47
+ });
48
+ case "kalshi-demo":
49
+ return new kalshi_demo_1.KalshiDemoExchange({
50
+ credentials: {
51
+ apiKey: credentials?.apiKey || process.env.KALSHI_API_KEY,
52
+ privateKey: credentials?.privateKey || process.env.KALSHI_PRIVATE_KEY,
53
+ },
54
+ });
55
+ case "probable":
56
+ return new probable_1.ProbableExchange({
57
+ apiKey: credentials?.apiKey || process.env.PROBABLE_API_KEY,
58
+ apiSecret: credentials?.apiSecret || process.env.PROBABLE_API_SECRET,
59
+ passphrase: credentials?.passphrase || process.env.PROBABLE_PASSPHRASE,
60
+ privateKey: credentials?.privateKey || process.env.PROBABLE_PRIVATE_KEY,
61
+ });
62
+ case "baozi":
63
+ return new baozi_1.BaoziExchange({
64
+ privateKey: credentials?.privateKey || process.env.BAOZI_PRIVATE_KEY,
65
+ });
66
+ case "myriad":
67
+ return new myriad_1.MyriadExchange({
68
+ apiKey: credentials?.apiKey ||
69
+ process.env.MYRIAD_API_KEY ||
70
+ process.env.MYRIAD_PROD,
71
+ privateKey: credentials?.privateKey || process.env.MYRIAD_WALLET_ADDRESS,
72
+ });
73
+ case "opinion":
74
+ return new opinion_1.OpinionExchange({
75
+ apiKey: credentials?.apiKey || process.env.OPINION_API_KEY,
76
+ privateKey: credentials?.privateKey || process.env.OPINION_PRIVATE_KEY,
77
+ funderAddress: credentials?.funderAddress,
78
+ });
79
+ case "metaculus":
80
+ return new metaculus_1.MetaculusExchange({
81
+ apiToken: credentials?.apiToken || process.env.METACULUS_API_TOKEN,
82
+ });
83
+ case "smarkets":
84
+ return new smarkets_1.SmarketsExchange({
85
+ apiKey: credentials?.apiKey || process.env.SMARKETS_EMAIL,
86
+ privateKey: credentials?.privateKey || process.env.SMARKETS_PASSWORD,
87
+ });
88
+ case "polymarket_us":
89
+ return new polymarket_us_1.PolymarketUSExchange({
90
+ apiKey: credentials?.apiKey || process.env.POLYMARKET_US_KEY_ID,
91
+ privateKey: credentials?.privateKey || process.env.POLYMARKET_US_SECRET_KEY,
92
+ });
93
+ case "router":
94
+ return new router_1.Router({
95
+ apiKey: bearerToken,
96
+ });
97
+ default:
98
+ throw new Error(`Unknown exchange: ${name}`);
99
+ }
100
+ }
@@ -304,6 +304,21 @@
304
304
  }
305
305
  ]
306
306
  },
307
+ "watchOrderBooks": {
308
+ "verb": "post",
309
+ "args": [
310
+ {
311
+ "name": "ids",
312
+ "kind": "unknown",
313
+ "optional": false
314
+ },
315
+ {
316
+ "name": "limit",
317
+ "kind": "number",
318
+ "optional": true
319
+ }
320
+ ]
321
+ },
307
322
  "unwatchOrderBook": {
308
323
  "verb": "post",
309
324
  "args": [
@@ -1329,6 +1329,51 @@ paths:
1329
1329
  description: >-
1330
1330
  Watch order book updates in real-time via WebSocket. Returns a promise that resolves with the next order book
1331
1331
  update. Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1332
+ '/api/{exchange}/watchOrderBooks':
1333
+ post:
1334
+ summary: Watch Order Books
1335
+ operationId: watchOrderBooks
1336
+ parameters:
1337
+ - $ref: '#/components/parameters/ExchangeParam'
1338
+ requestBody:
1339
+ content:
1340
+ application/json:
1341
+ schema:
1342
+ title: WatchOrderBooksRequest
1343
+ type: object
1344
+ properties:
1345
+ args:
1346
+ type: array
1347
+ minItems: 1
1348
+ maxItems: 2
1349
+ items:
1350
+ oneOf:
1351
+ - type: array
1352
+ items:
1353
+ type: string
1354
+ - type: number
1355
+ credentials:
1356
+ $ref: '#/components/schemas/ExchangeCredentials'
1357
+ required:
1358
+ - args
1359
+ responses:
1360
+ '200':
1361
+ description: Watch Order Books response
1362
+ content:
1363
+ application/json:
1364
+ schema:
1365
+ allOf:
1366
+ - $ref: '#/components/schemas/BaseResponse'
1367
+ - type: object
1368
+ properties:
1369
+ data:
1370
+ type: object
1371
+ additionalProperties:
1372
+ $ref: '#/components/schemas/OrderBook'
1373
+ description: >-
1374
+ Watch multiple order books simultaneously via WebSocket. Returns a promise that resolves with a record of order
1375
+ book snapshots keyed by ID. Exchanges with native batch support (e.g. Kalshi) send a single subscribe message
1376
+ for all tickers; others fall back to individual watchOrderBook calls.
1332
1377
  '/api/{exchange}/unwatchOrderBook':
1333
1378
  post:
1334
1379
  summary: Unwatch Order Book
@@ -0,0 +1,20 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { Server as HttpServer } from "http";
3
+ export interface CreateWebSocketHandlerOptions {
4
+ /** Access token for authentication (same as x-pmxt-access-token). */
5
+ accessToken?: string;
6
+ }
7
+ /**
8
+ * Create a WebSocket handler that can be attached to an HTTP server.
9
+ *
10
+ * Usage:
11
+ * ```ts
12
+ * const server = app.listen(port);
13
+ * const wss = createWebSocketHandler({ accessToken });
14
+ * wss.attach(server);
15
+ * ```
16
+ */
17
+ export declare function createWebSocketHandler(options?: CreateWebSocketHandlerOptions): {
18
+ wss: WebSocketServer;
19
+ attach: (server: HttpServer) => void;
20
+ };
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWebSocketHandler = createWebSocketHandler;
4
+ const ws_1 = require("ws");
5
+ const errors_1 = require("../errors");
6
+ const exchange_factory_1 = require("./exchange-factory");
7
+ // ---------------------------------------------------------------------------
8
+ // Streaming methods
9
+ // ---------------------------------------------------------------------------
10
+ /** Set of method names that produce streaming data. */
11
+ const WATCH_METHODS = new Set([
12
+ "watchOrderBook",
13
+ "watchOrderBooks",
14
+ "watchTrades",
15
+ ]);
16
+ /** Methods for unsubscribing. */
17
+ const UNWATCH_METHODS = {
18
+ unwatchOrderBook: "watchOrderBook",
19
+ };
20
+ function send(ws, msg) {
21
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
22
+ ws.send(JSON.stringify(msg));
23
+ }
24
+ }
25
+ function sendError(ws, id, message, code) {
26
+ send(ws, { event: "error", id, error: { message, code } });
27
+ }
28
+ /**
29
+ * Build a unique key for a subscription so we can cancel it.
30
+ */
31
+ function subscriptionKey(msg) {
32
+ return `${msg.exchange}:${msg.method}:${JSON.stringify(msg.args)}`;
33
+ }
34
+ /**
35
+ * Start a streaming loop for a single-ticker watch method
36
+ * (watchOrderBook, watchTrades).
37
+ */
38
+ async function streamSingle(exchange, method, args, id, ws, signal) {
39
+ const symbol = typeof args[0] === "string" ? args[0] : String(args[0]);
40
+ // Send subscribed acknowledgement
41
+ send(ws, { event: "subscribed", id });
42
+ while (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
43
+ try {
44
+ const result = await exchange[method](...args);
45
+ if (signal.aborted)
46
+ break;
47
+ send(ws, { id, event: "data", method, symbol, data: result });
48
+ }
49
+ catch (err) {
50
+ if (signal.aborted)
51
+ break;
52
+ const message = err instanceof errors_1.BaseError
53
+ ? err.message
54
+ : err instanceof Error
55
+ ? err.message
56
+ : "Unknown streaming error";
57
+ const code = err instanceof errors_1.BaseError ? err.code : undefined;
58
+ sendError(ws, id, message, code);
59
+ // Brief pause before retry to avoid tight error loops
60
+ await new Promise((r) => setTimeout(r, 1000));
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Start a streaming loop for the batch watchOrderBooks method.
66
+ * Each update is sent as an individual data message per symbol.
67
+ */
68
+ async function streamBatch(exchange, method, args, id, ws, signal) {
69
+ const ids = Array.isArray(args[0]) ? args[0] : [];
70
+ send(ws, { event: "subscribed", id });
71
+ while (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
72
+ try {
73
+ const result = await exchange[method](...args);
74
+ if (signal.aborted)
75
+ break;
76
+ for (const [symbol, data] of Object.entries(result)) {
77
+ send(ws, { id, event: "data", method, symbol, data });
78
+ }
79
+ }
80
+ catch (err) {
81
+ if (signal.aborted)
82
+ break;
83
+ const message = err instanceof errors_1.BaseError
84
+ ? err.message
85
+ : err instanceof Error
86
+ ? err.message
87
+ : "Unknown streaming error";
88
+ const code = err instanceof errors_1.BaseError ? err.code : undefined;
89
+ sendError(ws, id, message, code);
90
+ await new Promise((r) => setTimeout(r, 1000));
91
+ }
92
+ }
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Public API
96
+ // ---------------------------------------------------------------------------
97
+ /**
98
+ * Create a WebSocket handler that can be attached to an HTTP server.
99
+ *
100
+ * Usage:
101
+ * ```ts
102
+ * const server = app.listen(port);
103
+ * const wss = createWebSocketHandler({ accessToken });
104
+ * wss.attach(server);
105
+ * ```
106
+ */
107
+ function createWebSocketHandler(options = {}) {
108
+ const { accessToken } = options;
109
+ const wss = new ws_1.WebSocketServer({ noServer: true });
110
+ function attach(server) {
111
+ server.on("upgrade", (request, socket, head) => {
112
+ const url = new URL(request.url || "/", `http://${request.headers.host}`);
113
+ // Only handle upgrades to /ws
114
+ if (url.pathname !== "/ws") {
115
+ socket.destroy();
116
+ return;
117
+ }
118
+ // Validate access token from query parameter if configured
119
+ if (accessToken) {
120
+ const token = url.searchParams.get("token") ||
121
+ request.headers["x-pmxt-access-token"];
122
+ if (!token || token !== accessToken) {
123
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
124
+ socket.destroy();
125
+ return;
126
+ }
127
+ }
128
+ wss.handleUpgrade(request, socket, head, (ws) => {
129
+ wss.emit("connection", ws, request);
130
+ });
131
+ });
132
+ }
133
+ wss.on("connection", (ws) => {
134
+ const state = {
135
+ subscriptions: new Map(),
136
+ exchanges: new Map(),
137
+ authenticated: !accessToken, // pre-authed if no token required
138
+ };
139
+ ws.on("message", (raw) => {
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
143
+ }
144
+ catch {
145
+ sendError(ws, undefined, "Invalid JSON");
146
+ return;
147
+ }
148
+ const id = parsed.id;
149
+ const action = parsed.action;
150
+ const exchange = parsed.exchange;
151
+ if (!id || !action || !exchange) {
152
+ sendError(ws, id, "Missing required fields: id, action, exchange");
153
+ return;
154
+ }
155
+ const exchangeName = exchange.toLowerCase();
156
+ if (action === "subscribe") {
157
+ const msg = {
158
+ id,
159
+ action: "subscribe",
160
+ exchange: exchangeName,
161
+ method: parsed.method,
162
+ args: parsed.args || [],
163
+ credentials: parsed.credentials,
164
+ };
165
+ handleSubscribe(ws, state, msg, exchangeName);
166
+ }
167
+ else if (action === "unsubscribe") {
168
+ const msg = {
169
+ id,
170
+ action: "unsubscribe",
171
+ exchange: exchangeName,
172
+ method: parsed.method,
173
+ args: parsed.args || [],
174
+ };
175
+ handleUnsubscribe(ws, state, msg, exchangeName);
176
+ }
177
+ else {
178
+ sendError(ws, id, `Unknown action: ${action}`);
179
+ }
180
+ });
181
+ ws.on("close", () => {
182
+ // Abort all active subscriptions
183
+ for (const [, sub] of state.subscriptions) {
184
+ sub.abortController.abort();
185
+ }
186
+ state.subscriptions.clear();
187
+ // Close all exchange instances
188
+ for (const [, exchange] of state.exchanges) {
189
+ if (typeof exchange.close === "function") {
190
+ exchange.close().catch(() => { });
191
+ }
192
+ }
193
+ state.exchanges.clear();
194
+ });
195
+ ws.on("error", () => {
196
+ // Abort subscriptions on error too
197
+ for (const [, sub] of state.subscriptions) {
198
+ sub.abortController.abort();
199
+ }
200
+ });
201
+ });
202
+ return { wss, attach };
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Handlers
206
+ // ---------------------------------------------------------------------------
207
+ function getOrCreateExchange(state, exchangeName, credentials) {
208
+ // Use credentials as part of the cache key to support multiple auths
209
+ const cacheKey = credentials && (credentials.apiKey || credentials.privateKey)
210
+ ? `${exchangeName}:${credentials.apiKey || ""}:${credentials.privateKey || ""}`
211
+ : exchangeName;
212
+ const existing = state.exchanges.get(cacheKey);
213
+ if (existing)
214
+ return existing;
215
+ const exchange = (0, exchange_factory_1.createExchange)(exchangeName, credentials);
216
+ state.exchanges.set(cacheKey, exchange);
217
+ return exchange;
218
+ }
219
+ function handleSubscribe(ws, state, msg, exchangeName) {
220
+ const { id, method, args, credentials } = msg;
221
+ if (!WATCH_METHODS.has(method)) {
222
+ sendError(ws, id, `Method '${method}' is not a streaming method. Use the HTTP API for non-streaming calls.`);
223
+ return;
224
+ }
225
+ const key = subscriptionKey(msg);
226
+ // Already subscribed
227
+ if (state.subscriptions.has(key)) {
228
+ sendError(ws, id, `Already subscribed to ${key}`);
229
+ return;
230
+ }
231
+ let exchange;
232
+ try {
233
+ exchange = getOrCreateExchange(state, exchangeName, credentials);
234
+ }
235
+ catch (err) {
236
+ const message = err instanceof Error ? err.message : "Failed to create exchange";
237
+ sendError(ws, id, message);
238
+ return;
239
+ }
240
+ if (typeof exchange[method] !== "function") {
241
+ sendError(ws, id, `Method '${method}' not found on ${exchangeName}`);
242
+ return;
243
+ }
244
+ const abortController = new AbortController();
245
+ state.subscriptions.set(key, { abortController });
246
+ const streamFn = method === "watchOrderBooks" ? streamBatch : streamSingle;
247
+ // Fire and forget -- the loop runs until aborted or WS closes
248
+ streamFn(exchange, method, args || [], id, ws, abortController.signal).catch(() => {
249
+ // Stream ended -- clean up
250
+ state.subscriptions.delete(key);
251
+ });
252
+ }
253
+ function handleUnsubscribe(ws, state, msg, exchangeName) {
254
+ const { id } = msg;
255
+ // Build the key of the subscription to cancel.
256
+ // For unsubscribe actions, the corresponding subscribe key uses the watch method.
257
+ const watchMethod = UNWATCH_METHODS[msg.method] || msg.method;
258
+ const lookupMsg = { ...msg, method: watchMethod };
259
+ const key = subscriptionKey(lookupMsg);
260
+ const sub = state.subscriptions.get(key);
261
+ if (!sub) {
262
+ sendError(ws, id, `No active subscription for ${key}`);
263
+ return;
264
+ }
265
+ sub.abortController.abort();
266
+ state.subscriptions.delete(key);
267
+ send(ws, { event: "subscribed", id }); // Acknowledge unsubscribe
268
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxt-core",
3
- "version": "2.35.30",
3
+ "version": "2.35.32",
4
4
  "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  "test": "jest -c jest.config.js",
30
30
  "server": "tsx watch src/server/index.ts",
31
31
  "server:prod": "node dist/server/index.js",
32
- "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.35.30,library=urllib3",
33
- "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.35.30,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
32
+ "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.35.32,library=urllib3",
33
+ "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.35.32,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
34
34
  "fetch:openapi": "node scripts/fetch-openapi-specs.js",
35
35
  "extract:jsdoc": "node ../scripts/extract-jsdoc.js",
36
36
  "generate:docs": "npm run extract:jsdoc && node ../scripts/generate-api-docs.js",