pmxtjs 2.51.1 → 2.51.2

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.
@@ -17,13 +17,13 @@
17
17
  * console.log(markets[0].title);
18
18
  * ```
19
19
  */
20
- import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Rain, Mock } from "./pmxt/client.js";
20
+ import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Rain, Hunch, Mock } from "./pmxt/client.js";
21
21
  import { Router } from "./pmxt/router.js";
22
22
  import { ServerManager } from "./pmxt/server-manager.js";
23
23
  import { FeedClient } from "./pmxt/feed-client.js";
24
24
  import * as models from "./pmxt/models.js";
25
25
  import * as errors from "./pmxt/errors.js";
26
- export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock, PolymarketOptions } from "./pmxt/client.js";
26
+ export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock, PolymarketOptions } from "./pmxt/client.js";
27
27
  export { FeedClient } from "./pmxt/feed-client.js";
28
28
  export type { Ticker, Tickers, OHLCV, Market as FeedMarket, OracleRound, FeedClientOptions } from "./pmxt/feed-client.js";
29
29
  export { Router } from "./pmxt/router.js";
@@ -97,6 +97,7 @@ declare const pmxt: {
97
97
  SuiBets: typeof SuiBets;
98
98
  Suibets: typeof SuiBets;
99
99
  Rain: typeof Rain;
100
+ Hunch: typeof Hunch;
100
101
  Mock: typeof Mock;
101
102
  Router: typeof Router;
102
103
  ServerManager: typeof ServerManager;
package/dist/esm/index.js CHANGED
@@ -17,13 +17,13 @@
17
17
  * console.log(markets[0].title);
18
18
  * ```
19
19
  */
20
- import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock } from "./pmxt/client.js";
20
+ import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock } from "./pmxt/client.js";
21
21
  import { Router } from "./pmxt/router.js";
22
22
  import { ServerManager } from "./pmxt/server-manager.js";
23
23
  import { FeedClient } from "./pmxt/feed-client.js";
24
24
  import * as models from "./pmxt/models.js";
25
25
  import * as errors from "./pmxt/errors.js";
26
- export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock } from "./pmxt/client.js";
26
+ export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock } from "./pmxt/client.js";
27
27
  export { FeedClient } from "./pmxt/feed-client.js";
28
28
  export { Router } from "./pmxt/router.js";
29
29
  export { ServerManager } from "./pmxt/server-manager.js";
@@ -78,6 +78,7 @@ const pmxt = {
78
78
  SuiBets,
79
79
  Suibets,
80
80
  Rain,
81
+ Hunch,
81
82
  Mock,
82
83
  Router,
83
84
  ServerManager,
@@ -105,6 +105,7 @@ export declare abstract class Exchange {
105
105
  private _wsClient;
106
106
  /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
107
107
  private _wsUnsupported;
108
+ private readonly _useSidecarLockBaseUrl;
108
109
  constructor(exchangeName: string, options?: ExchangeOptions);
109
110
  private initializeServer;
110
111
  protected handleResponse(response: any): any;
@@ -125,9 +126,9 @@ export declare abstract class Exchange {
125
126
  /**
126
127
  * Resolve the current sidecar base URL.
127
128
  *
128
- * For hosted mode the configured basePath is returned as-is.
129
- * For local mode the port is re-read from the lock file on every
130
- * call so we pick up sidecar restarts that land on a different port.
129
+ * Hosted mode and explicit baseUrl clients keep the configured basePath.
130
+ * Default local clients re-read the lock-file port on every call so they
131
+ * pick up sidecar restarts that land on a different port.
131
132
  */
132
133
  private resolveBaseUrl;
133
134
  /**
@@ -576,6 +577,14 @@ export interface PolymarketOptions {
576
577
  proxyAddress?: string;
577
578
  /** Optional signature type */
578
579
  signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number;
580
+ /**
581
+ * EVM wallet address used for hosted reads/writes.
582
+ */
583
+ walletAddress?: string;
584
+ /**
585
+ * External signer used for hosted writes.
586
+ */
587
+ signer?: Signer;
579
588
  }
580
589
  export declare class Polymarket extends Exchange {
581
590
  constructor(options?: PolymarketOptions);
@@ -856,6 +865,15 @@ export declare const Suibets: typeof SuiBets;
856
865
  export declare class Rain extends Exchange {
857
866
  constructor(options?: ExchangeOptions);
858
867
  }
868
+ /**
869
+ * Hunch exchange client.
870
+ *
871
+ * Hunch is a crypto-native prediction market. Reads are unauthenticated;
872
+ * trading requires an EVM private key.
873
+ */
874
+ export declare class Hunch extends Exchange {
875
+ constructor(options?: ExchangeOptions);
876
+ }
859
877
  /**
860
878
  * Mock exchange client.
861
879
  *
@@ -7,8 +7,8 @@
7
7
  import { Configuration, DefaultApi, } from "../generated/src/index.js";
8
8
  import { MarketList, } from "./models.js";
9
9
  import { ServerManager } from "./server-manager.js";
10
- import { PmxtError, fromServerError, InvalidOrder } from "./errors.js";
11
- import { resolvePmxtBaseUrl } from "./constants.js";
10
+ import { PmxtError, fromServerError, InvalidOrder, NotSupported } from "./errors.js";
11
+ import { ENV, resolvePmxtBaseUrl } from "./constants.js";
12
12
  import { SidecarWsClient } from "./ws-client.js";
13
13
  import { logger } from "./logger.js";
14
14
  // Hosted-mode trading dispatch.
@@ -190,6 +190,7 @@ export class Exchange {
190
190
  _wsClient = null;
191
191
  /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
192
192
  _wsUnsupported = false;
193
+ _useSidecarLockBaseUrl;
193
194
  constructor(exchangeName, options = {}) {
194
195
  this.exchangeName = exchangeName.toLowerCase();
195
196
  this.apiKey = options.apiKey;
@@ -207,6 +208,9 @@ export class Exchange {
207
208
  const baseUrl = resolved.baseUrl;
208
209
  this.pmxtApiKey = resolved.pmxtApiKey;
209
210
  this.isHosted = resolved.isHosted;
211
+ const hasBaseUrlOverride = Boolean(options.baseUrl ||
212
+ (typeof process !== "undefined" && process.env[ENV.BASE_URL]));
213
+ this._useSidecarLockBaseUrl = !this.isHosted && !hasBaseUrlOverride;
210
214
  // Hosted trading bridge: if the caller passed a privateKey but no
211
215
  // explicit signer, lazily wrap it in an EthersSigner so that
212
216
  // `pmxt.Polymarket(pmxtApiKey, privateKey)` just works without the
@@ -247,15 +251,17 @@ export class Exchange {
247
251
  if (autoStartServer) {
248
252
  try {
249
253
  await this.serverManager.ensureServerRunning();
250
- // Get the actual port the server is running on
251
- // (may differ from default if default port was busy)
252
- const actualPort = this.serverManager.getRunningPort();
253
- const newBaseUrl = `http://localhost:${actualPort}`;
254
- // Update API client with actual base URL
255
- this.config = new Configuration({
256
- basePath: newBaseUrl,
257
- });
258
- this.api = new DefaultApi(this.config);
254
+ if (this._useSidecarLockBaseUrl) {
255
+ // Get the actual port the server is running on
256
+ // (may differ from default if default port was busy)
257
+ const actualPort = this.serverManager.getRunningPort();
258
+ const newBaseUrl = `http://localhost:${actualPort}`;
259
+ // Update API client with actual base URL
260
+ this.config = new Configuration({
261
+ basePath: newBaseUrl,
262
+ });
263
+ this.api = new DefaultApi(this.config);
264
+ }
259
265
  }
260
266
  catch (error) {
261
267
  const msg = `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
@@ -329,12 +335,12 @@ export class Exchange {
329
335
  /**
330
336
  * Resolve the current sidecar base URL.
331
337
  *
332
- * For hosted mode the configured basePath is returned as-is.
333
- * For local mode the port is re-read from the lock file on every
334
- * call so we pick up sidecar restarts that land on a different port.
338
+ * Hosted mode and explicit baseUrl clients keep the configured basePath.
339
+ * Default local clients re-read the lock-file port on every call so they
340
+ * pick up sidecar restarts that land on a different port.
335
341
  */
336
342
  resolveBaseUrl() {
337
- if (this.isHosted)
343
+ if (this.isHosted || !this._useSidecarLockBaseUrl)
338
344
  return this.config.basePath;
339
345
  const port = this.serverManager.getRunningPort();
340
346
  return `http://localhost:${port}`;
@@ -1018,7 +1024,10 @@ export class Exchange {
1018
1024
  const route = HOSTED_METHOD_ROUTES.get("fetchOrder");
1019
1025
  const path = formatRoutePath(route, { order_id: orderId });
1020
1026
  const data = await _tradingRequest(this, { method: route.method, path });
1021
- return orderFromV0(data);
1027
+ const payload = data && typeof data === "object" && "order" in data
1028
+ ? data.order
1029
+ : data;
1030
+ return orderFromV0(payload);
1022
1031
  }
1023
1032
  try {
1024
1033
  const args = [];
@@ -1124,6 +1133,9 @@ export class Exchange {
1124
1133
  }
1125
1134
  async fetchClosedOrders(params) {
1126
1135
  await this.initPromise;
1136
+ if (this.isHosted) {
1137
+ throw new NotSupported("fetchClosedOrders is not available in hosted trading mode.");
1138
+ }
1127
1139
  try {
1128
1140
  const args = [];
1129
1141
  if (params !== undefined)
@@ -1152,6 +1164,9 @@ export class Exchange {
1152
1164
  }
1153
1165
  async fetchAllOrders(params) {
1154
1166
  await this.initPromise;
1167
+ if (this.isHosted) {
1168
+ throw new NotSupported("fetchAllOrders is not available in hosted trading mode.");
1169
+ }
1155
1170
  try {
1156
1171
  const args = [];
1157
1172
  if (params !== undefined)
@@ -1185,7 +1200,9 @@ export class Exchange {
1185
1200
  const route = HOSTED_METHOD_ROUTES.get("fetchPositions");
1186
1201
  const path = formatRoutePath(route, { address: resolvedAddress });
1187
1202
  const data = await _tradingRequest(this, { method: route.method, path });
1188
- const list = Array.isArray(data) ? data : [];
1203
+ const list = Array.isArray(data)
1204
+ ? data
1205
+ : (data && Array.isArray(data.positions) ? data.positions : []);
1189
1206
  return list.map((p) => positionFromV0(p));
1190
1207
  }
1191
1208
  try {
@@ -1221,7 +1238,9 @@ export class Exchange {
1221
1238
  const route = HOSTED_METHOD_ROUTES.get("fetchBalance");
1222
1239
  const path = formatRoutePath(route, { address: resolvedAddress });
1223
1240
  const data = await _tradingRequest(this, { method: route.method, path });
1224
- const list = Array.isArray(data) ? data : [];
1241
+ const list = Array.isArray(data)
1242
+ ? data
1243
+ : (data && Array.isArray(data.balances) ? data.balances : []);
1225
1244
  return list.map((b) => balanceFromV0(b));
1226
1245
  }
1227
1246
  try {
@@ -3034,6 +3053,17 @@ export class Rain extends Exchange {
3034
3053
  super("rain", options);
3035
3054
  }
3036
3055
  }
3056
+ /**
3057
+ * Hunch exchange client.
3058
+ *
3059
+ * Hunch is a crypto-native prediction market. Reads are unauthenticated;
3060
+ * trading requires an EVM private key.
3061
+ */
3062
+ export class Hunch extends Exchange {
3063
+ constructor(options = {}) {
3064
+ super("hunch", options);
3065
+ }
3066
+ }
3037
3067
  /**
3038
3068
  * Mock exchange client.
3039
3069
  *
@@ -9,6 +9,80 @@
9
9
  * Mirrors `sdks/python/pmxt/escrow.py`.
10
10
  */
11
11
  import { _tradingRequest, formatRoutePath, HOSTED_METHOD_ROUTES, resolveWalletAddress, } from "./hosted-routing.js";
12
+ import { ValidationError } from "./errors.js";
13
+ const APPROVAL_TOKENS = new Set(["usdc", "ctf"]);
14
+ const WITHDRAW_ACTIONS = new Set(["request", "claim", "cancel"]);
15
+ const USDC_SCALE = 1000000n;
16
+ function isDecimalTokenId(token) {
17
+ return /^[0-9]+$/.test(token);
18
+ }
19
+ function normalizeApprovalToken(token) {
20
+ if (typeof token !== "string") {
21
+ throw new ValidationError("token must be 'usdc', 'ctf', or a decimal CTF token_id string", "token");
22
+ }
23
+ const candidate = token.trim();
24
+ const normalized = candidate.toLowerCase();
25
+ if (APPROVAL_TOKENS.has(normalized)) {
26
+ return normalized;
27
+ }
28
+ if (isDecimalTokenId(candidate)) {
29
+ return candidate;
30
+ }
31
+ throw new ValidationError("token must be 'usdc', 'ctf', or a decimal CTF token_id string", "token");
32
+ }
33
+ function normalizeAmountWei(amountWei) {
34
+ if (amountWei === undefined) {
35
+ return undefined;
36
+ }
37
+ if (typeof amountWei !== "bigint") {
38
+ throw new ValidationError("amount_wei must be a non-negative integer", "amount_wei");
39
+ }
40
+ if (amountWei < 0n) {
41
+ throw new ValidationError("amount_wei must be non-negative", "amount_wei");
42
+ }
43
+ return amountWei.toString();
44
+ }
45
+ function validateUsdcDecimal(value, field) {
46
+ const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(value);
47
+ if (!match) {
48
+ throw new ValidationError(`${field} must be a finite positive number`, field);
49
+ }
50
+ const fractional = match[2] ?? "";
51
+ if (fractional.length > 6) {
52
+ throw new ValidationError(`${field} precision exceeds 6 decimals; max precision for USDC is 0.000001`, field);
53
+ }
54
+ const scaled = BigInt(match[1]) * USDC_SCALE + BigInt(fractional.padEnd(6, "0"));
55
+ if (scaled <= 0n) {
56
+ throw new ValidationError(`${field} must be positive`, field);
57
+ }
58
+ }
59
+ function normalizeUsdcAmount(value, field = "amount") {
60
+ if (typeof value === "bigint") {
61
+ if (value <= 0n) {
62
+ throw new ValidationError(`${field} must be positive`, field);
63
+ }
64
+ return value.toString();
65
+ }
66
+ if (typeof value === "number") {
67
+ if (!Number.isFinite(value)) {
68
+ throw new ValidationError(`${field} must be a finite positive number`, field);
69
+ }
70
+ validateUsdcDecimal(String(value), field);
71
+ return value;
72
+ }
73
+ if (typeof value === "string") {
74
+ const trimmed = value.trim();
75
+ validateUsdcDecimal(trimmed, field);
76
+ return trimmed;
77
+ }
78
+ throw new ValidationError(`${field} must be a finite positive number`, field);
79
+ }
80
+ function normalizeWithdrawAction(action) {
81
+ if (typeof action === "string" && WITHDRAW_ACTIONS.has(action)) {
82
+ return action;
83
+ }
84
+ throw new ValidationError("action must be 'request', 'claim', or 'cancel'", "action");
85
+ }
12
86
  export class Escrow {
13
87
  client;
14
88
  constructor(client) {
@@ -19,13 +93,16 @@ export class Escrow {
19
93
  * `amountWei` is omitted, the server returns an unlimited approval.
20
94
  */
21
95
  async approveTx(token, amountWei) {
96
+ const address = resolveWalletAddress(this.client);
97
+ const approvalAmount = normalizeAmountWei(amountWei);
22
98
  const route = HOSTED_METHOD_ROUTES.get("escrowApproveTx");
23
99
  return _tradingRequest(this.client, {
24
100
  method: route.method,
25
101
  path: route.path,
26
102
  body: {
27
- token,
28
- amount_wei: amountWei === undefined ? null : amountWei.toString(),
103
+ token: normalizeApprovalToken(token),
104
+ user_address: address,
105
+ ...(approvalAmount === undefined ? {} : { amount_wei: approvalAmount }),
29
106
  },
30
107
  });
31
108
  }
@@ -34,12 +111,15 @@ export class Escrow {
34
111
  * grid). Accepts number, decimal string, or BigInt in micro-units.
35
112
  */
36
113
  async depositTx(amount) {
114
+ const address = resolveWalletAddress(this.client);
37
115
  const route = HOSTED_METHOD_ROUTES.get("escrowDepositTx");
38
116
  return _tradingRequest(this.client, {
39
117
  method: route.method,
40
118
  path: route.path,
41
119
  body: {
42
- amount: typeof amount === "bigint" ? amount.toString() : amount,
120
+ token: "usdc",
121
+ amount: normalizeUsdcAmount(amount),
122
+ user_address: address,
43
123
  },
44
124
  });
45
125
  }
@@ -49,16 +129,31 @@ export class Escrow {
49
129
  * the timelock, `cancel` aborts a pending request.
50
130
  */
51
131
  async withdrawTx(action, amount) {
132
+ const normalizedAction = normalizeWithdrawAction(action);
133
+ const address = resolveWalletAddress(this.client);
52
134
  const route = HOSTED_METHOD_ROUTES.get("escrowWithdrawTx");
53
- const normalizedAmount = amount === undefined
54
- ? null
55
- : typeof amount === "bigint"
56
- ? amount.toString()
57
- : amount;
135
+ if (normalizedAction === "request") {
136
+ if (amount === undefined) {
137
+ throw new ValidationError("amount is required when action='request'", "amount");
138
+ }
139
+ return _tradingRequest(this.client, {
140
+ method: route.method,
141
+ path: route.path,
142
+ body: {
143
+ action: normalizedAction,
144
+ token: "usdc",
145
+ amount: normalizeUsdcAmount(amount),
146
+ user_address: address,
147
+ },
148
+ });
149
+ }
150
+ if (amount !== undefined) {
151
+ throw new ValidationError(`amount must be omitted when action='${normalizedAction}'`, "amount");
152
+ }
58
153
  return _tradingRequest(this.client, {
59
154
  method: route.method,
60
155
  path: route.path,
61
- body: { action, amount: normalizedAmount },
156
+ body: { action: normalizedAction, token: "usdc", user_address: address },
62
157
  });
63
158
  }
64
159
  /**
@@ -67,12 +162,16 @@ export class Escrow {
67
162
  */
68
163
  async withdrawals(opts = {}) {
69
164
  const address = resolveWalletAddress(this.client, opts.address);
165
+ const include = (opts.include ?? "pending,events").trim();
166
+ if (!include) {
167
+ throw new ValidationError("include must not be empty", "include");
168
+ }
70
169
  const route = HOSTED_METHOD_ROUTES.get("escrowWithdrawals");
71
170
  const path = formatRoutePath(route, { address });
72
171
  return _tradingRequest(this.client, {
73
172
  method: route.method,
74
173
  path,
75
- params: { include: opts.include ?? "pending,events" },
174
+ params: { include },
76
175
  });
77
176
  }
78
177
  }
@@ -81,12 +81,14 @@ export class ServerManager {
81
81
  * Check if the server is running.
82
82
  */
83
83
  async isServerRunning() {
84
- // Read lock file to get current port
85
- const port = this.getRunningPort();
84
+ const info = this.getServerInfo();
85
+ if (!info?.port) {
86
+ return false;
87
+ }
86
88
  try {
87
89
  // Use native fetch to check health on the actual running port
88
90
  // This avoids issues where this.api is configured with the wrong port
89
- const response = await fetch(`http://localhost:${port}/health`, {
91
+ const response = await fetch(`http://localhost:${info.port}/health`, {
90
92
  signal: AbortSignal.timeout(5_000),
91
93
  });
92
94
  if (response.ok) {
package/dist/index.d.ts CHANGED
@@ -17,13 +17,13 @@
17
17
  * console.log(markets[0].title);
18
18
  * ```
19
19
  */
20
- import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Rain, Mock } from "./pmxt/client.js";
20
+ import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Rain, Hunch, Mock } from "./pmxt/client.js";
21
21
  import { Router } from "./pmxt/router.js";
22
22
  import { ServerManager } from "./pmxt/server-manager.js";
23
23
  import { FeedClient } from "./pmxt/feed-client.js";
24
24
  import * as models from "./pmxt/models.js";
25
25
  import * as errors from "./pmxt/errors.js";
26
- export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock, PolymarketOptions } from "./pmxt/client.js";
26
+ export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock, PolymarketOptions } from "./pmxt/client.js";
27
27
  export { FeedClient } from "./pmxt/feed-client.js";
28
28
  export type { Ticker, Tickers, OHLCV, Market as FeedMarket, OracleRound, FeedClientOptions } from "./pmxt/feed-client.js";
29
29
  export { Router } from "./pmxt/router.js";
@@ -97,6 +97,7 @@ declare const pmxt: {
97
97
  SuiBets: typeof SuiBets;
98
98
  Suibets: typeof SuiBets;
99
99
  Rain: typeof Rain;
100
+ Hunch: typeof Hunch;
100
101
  Mock: typeof Mock;
101
102
  Router: typeof Router;
102
103
  ServerManager: typeof ServerManager;
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
55
55
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
56
56
  };
57
57
  Object.defineProperty(exports, "__esModule", { value: true });
58
- exports.server = exports.MarketList = exports.LIMITLESS_VENUE_ESCROW_ADDRESSES = exports.VENUE_ESCROW_ADDRESSES = exports.PREFUNDED_ESCROW_ADDRESSES = exports.resolvePmxtBaseUrl = exports.ENV = exports.LOCAL_URL = exports.HOSTED_URL = exports.ServerManager = exports.Router = exports.FeedClient = exports.Mock = exports.Rain = exports.Suibets = exports.SuiBets = exports.Hyperliquid = exports.GeminiTitan = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.Limitless = exports.KalshiDemo = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
58
+ exports.server = exports.MarketList = exports.LIMITLESS_VENUE_ESCROW_ADDRESSES = exports.VENUE_ESCROW_ADDRESSES = exports.PREFUNDED_ESCROW_ADDRESSES = exports.resolvePmxtBaseUrl = exports.ENV = exports.LOCAL_URL = exports.HOSTED_URL = exports.ServerManager = exports.Router = exports.FeedClient = exports.Mock = exports.Hunch = exports.Rain = exports.Suibets = exports.SuiBets = exports.Hyperliquid = exports.GeminiTitan = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.Limitless = exports.KalshiDemo = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
59
59
  const client_js_1 = require("./pmxt/client.js");
60
60
  const router_js_1 = require("./pmxt/router.js");
61
61
  const server_manager_js_1 = require("./pmxt/server-manager.js");
@@ -80,6 +80,7 @@ Object.defineProperty(exports, "Hyperliquid", { enumerable: true, get: function
80
80
  Object.defineProperty(exports, "SuiBets", { enumerable: true, get: function () { return client_js_2.SuiBets; } });
81
81
  Object.defineProperty(exports, "Suibets", { enumerable: true, get: function () { return client_js_2.Suibets; } });
82
82
  Object.defineProperty(exports, "Rain", { enumerable: true, get: function () { return client_js_2.Rain; } });
83
+ Object.defineProperty(exports, "Hunch", { enumerable: true, get: function () { return client_js_2.Hunch; } });
83
84
  Object.defineProperty(exports, "Mock", { enumerable: true, get: function () { return client_js_2.Mock; } });
84
85
  var feed_client_js_2 = require("./pmxt/feed-client.js");
85
86
  Object.defineProperty(exports, "FeedClient", { enumerable: true, get: function () { return feed_client_js_2.FeedClient; } });
@@ -146,6 +147,7 @@ const pmxt = {
146
147
  SuiBets: client_js_1.SuiBets,
147
148
  Suibets: client_js_1.Suibets,
148
149
  Rain: client_js_1.Rain,
150
+ Hunch: client_js_1.Hunch,
149
151
  Mock: client_js_1.Mock,
150
152
  Router: router_js_1.Router,
151
153
  ServerManager: server_manager_js_1.ServerManager,
@@ -105,6 +105,7 @@ export declare abstract class Exchange {
105
105
  private _wsClient;
106
106
  /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
107
107
  private _wsUnsupported;
108
+ private readonly _useSidecarLockBaseUrl;
108
109
  constructor(exchangeName: string, options?: ExchangeOptions);
109
110
  private initializeServer;
110
111
  protected handleResponse(response: any): any;
@@ -125,9 +126,9 @@ export declare abstract class Exchange {
125
126
  /**
126
127
  * Resolve the current sidecar base URL.
127
128
  *
128
- * For hosted mode the configured basePath is returned as-is.
129
- * For local mode the port is re-read from the lock file on every
130
- * call so we pick up sidecar restarts that land on a different port.
129
+ * Hosted mode and explicit baseUrl clients keep the configured basePath.
130
+ * Default local clients re-read the lock-file port on every call so they
131
+ * pick up sidecar restarts that land on a different port.
131
132
  */
132
133
  private resolveBaseUrl;
133
134
  /**
@@ -576,6 +577,14 @@ export interface PolymarketOptions {
576
577
  proxyAddress?: string;
577
578
  /** Optional signature type */
578
579
  signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number;
580
+ /**
581
+ * EVM wallet address used for hosted reads/writes.
582
+ */
583
+ walletAddress?: string;
584
+ /**
585
+ * External signer used for hosted writes.
586
+ */
587
+ signer?: Signer;
579
588
  }
580
589
  export declare class Polymarket extends Exchange {
581
590
  constructor(options?: PolymarketOptions);
@@ -856,6 +865,15 @@ export declare const Suibets: typeof SuiBets;
856
865
  export declare class Rain extends Exchange {
857
866
  constructor(options?: ExchangeOptions);
858
867
  }
868
+ /**
869
+ * Hunch exchange client.
870
+ *
871
+ * Hunch is a crypto-native prediction market. Reads are unauthenticated;
872
+ * trading requires an EVM private key.
873
+ */
874
+ export declare class Hunch extends Exchange {
875
+ constructor(options?: ExchangeOptions);
876
+ }
859
877
  /**
860
878
  * Mock exchange client.
861
879
  *
@@ -6,7 +6,7 @@
6
6
  * OpenAPI client, matching the Python API exactly.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Mock = exports.Rain = exports.Suibets = exports.SuiBets = exports.Hyperliquid = exports.GeminiTitan = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.KalshiDemo = exports.Limitless = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
9
+ exports.Mock = exports.Hunch = exports.Rain = exports.Suibets = exports.SuiBets = exports.Hyperliquid = exports.GeminiTitan = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.KalshiDemo = exports.Limitless = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
10
10
  const index_js_1 = require("../generated/src/index.js");
11
11
  const models_js_1 = require("./models.js");
12
12
  const server_manager_js_1 = require("./server-manager.js");
@@ -193,6 +193,7 @@ class Exchange {
193
193
  _wsClient = null;
194
194
  /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
195
195
  _wsUnsupported = false;
196
+ _useSidecarLockBaseUrl;
196
197
  constructor(exchangeName, options = {}) {
197
198
  this.exchangeName = exchangeName.toLowerCase();
198
199
  this.apiKey = options.apiKey;
@@ -210,6 +211,9 @@ class Exchange {
210
211
  const baseUrl = resolved.baseUrl;
211
212
  this.pmxtApiKey = resolved.pmxtApiKey;
212
213
  this.isHosted = resolved.isHosted;
214
+ const hasBaseUrlOverride = Boolean(options.baseUrl ||
215
+ (typeof process !== "undefined" && process.env[constants_js_1.ENV.BASE_URL]));
216
+ this._useSidecarLockBaseUrl = !this.isHosted && !hasBaseUrlOverride;
213
217
  // Hosted trading bridge: if the caller passed a privateKey but no
214
218
  // explicit signer, lazily wrap it in an EthersSigner so that
215
219
  // `pmxt.Polymarket(pmxtApiKey, privateKey)` just works without the
@@ -250,15 +254,17 @@ class Exchange {
250
254
  if (autoStartServer) {
251
255
  try {
252
256
  await this.serverManager.ensureServerRunning();
253
- // Get the actual port the server is running on
254
- // (may differ from default if default port was busy)
255
- const actualPort = this.serverManager.getRunningPort();
256
- const newBaseUrl = `http://localhost:${actualPort}`;
257
- // Update API client with actual base URL
258
- this.config = new index_js_1.Configuration({
259
- basePath: newBaseUrl,
260
- });
261
- this.api = new index_js_1.DefaultApi(this.config);
257
+ if (this._useSidecarLockBaseUrl) {
258
+ // Get the actual port the server is running on
259
+ // (may differ from default if default port was busy)
260
+ const actualPort = this.serverManager.getRunningPort();
261
+ const newBaseUrl = `http://localhost:${actualPort}`;
262
+ // Update API client with actual base URL
263
+ this.config = new index_js_1.Configuration({
264
+ basePath: newBaseUrl,
265
+ });
266
+ this.api = new index_js_1.DefaultApi(this.config);
267
+ }
262
268
  }
263
269
  catch (error) {
264
270
  const msg = `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
@@ -332,12 +338,12 @@ class Exchange {
332
338
  /**
333
339
  * Resolve the current sidecar base URL.
334
340
  *
335
- * For hosted mode the configured basePath is returned as-is.
336
- * For local mode the port is re-read from the lock file on every
337
- * call so we pick up sidecar restarts that land on a different port.
341
+ * Hosted mode and explicit baseUrl clients keep the configured basePath.
342
+ * Default local clients re-read the lock-file port on every call so they
343
+ * pick up sidecar restarts that land on a different port.
338
344
  */
339
345
  resolveBaseUrl() {
340
- if (this.isHosted)
346
+ if (this.isHosted || !this._useSidecarLockBaseUrl)
341
347
  return this.config.basePath;
342
348
  const port = this.serverManager.getRunningPort();
343
349
  return `http://localhost:${port}`;
@@ -1021,7 +1027,10 @@ class Exchange {
1021
1027
  const route = hosted_routing_js_1.HOSTED_METHOD_ROUTES.get("fetchOrder");
1022
1028
  const path = (0, hosted_routing_js_1.formatRoutePath)(route, { order_id: orderId });
1023
1029
  const data = await (0, hosted_routing_js_1._tradingRequest)(this, { method: route.method, path });
1024
- return (0, hosted_mappers_js_1.orderFromV0)(data);
1030
+ const payload = data && typeof data === "object" && "order" in data
1031
+ ? data.order
1032
+ : data;
1033
+ return (0, hosted_mappers_js_1.orderFromV0)(payload);
1025
1034
  }
1026
1035
  try {
1027
1036
  const args = [];
@@ -1127,6 +1136,9 @@ class Exchange {
1127
1136
  }
1128
1137
  async fetchClosedOrders(params) {
1129
1138
  await this.initPromise;
1139
+ if (this.isHosted) {
1140
+ throw new errors_js_1.NotSupported("fetchClosedOrders is not available in hosted trading mode.");
1141
+ }
1130
1142
  try {
1131
1143
  const args = [];
1132
1144
  if (params !== undefined)
@@ -1155,6 +1167,9 @@ class Exchange {
1155
1167
  }
1156
1168
  async fetchAllOrders(params) {
1157
1169
  await this.initPromise;
1170
+ if (this.isHosted) {
1171
+ throw new errors_js_1.NotSupported("fetchAllOrders is not available in hosted trading mode.");
1172
+ }
1158
1173
  try {
1159
1174
  const args = [];
1160
1175
  if (params !== undefined)
@@ -1188,7 +1203,9 @@ class Exchange {
1188
1203
  const route = hosted_routing_js_1.HOSTED_METHOD_ROUTES.get("fetchPositions");
1189
1204
  const path = (0, hosted_routing_js_1.formatRoutePath)(route, { address: resolvedAddress });
1190
1205
  const data = await (0, hosted_routing_js_1._tradingRequest)(this, { method: route.method, path });
1191
- const list = Array.isArray(data) ? data : [];
1206
+ const list = Array.isArray(data)
1207
+ ? data
1208
+ : (data && Array.isArray(data.positions) ? data.positions : []);
1192
1209
  return list.map((p) => (0, hosted_mappers_js_1.positionFromV0)(p));
1193
1210
  }
1194
1211
  try {
@@ -1224,7 +1241,9 @@ class Exchange {
1224
1241
  const route = hosted_routing_js_1.HOSTED_METHOD_ROUTES.get("fetchBalance");
1225
1242
  const path = (0, hosted_routing_js_1.formatRoutePath)(route, { address: resolvedAddress });
1226
1243
  const data = await (0, hosted_routing_js_1._tradingRequest)(this, { method: route.method, path });
1227
- const list = Array.isArray(data) ? data : [];
1244
+ const list = Array.isArray(data)
1245
+ ? data
1246
+ : (data && Array.isArray(data.balances) ? data.balances : []);
1228
1247
  return list.map((b) => (0, hosted_mappers_js_1.balanceFromV0)(b));
1229
1248
  }
1230
1249
  try {
@@ -3053,6 +3072,18 @@ class Rain extends Exchange {
3053
3072
  }
3054
3073
  }
3055
3074
  exports.Rain = Rain;
3075
+ /**
3076
+ * Hunch exchange client.
3077
+ *
3078
+ * Hunch is a crypto-native prediction market. Reads are unauthenticated;
3079
+ * trading requires an EVM private key.
3080
+ */
3081
+ class Hunch extends Exchange {
3082
+ constructor(options = {}) {
3083
+ super("hunch", options);
3084
+ }
3085
+ }
3086
+ exports.Hunch = Hunch;
3056
3087
  /**
3057
3088
  * Mock exchange client.
3058
3089
  *
@@ -12,6 +12,80 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.Escrow = void 0;
14
14
  const hosted_routing_1 = require("./hosted-routing");
15
+ const errors_1 = require("./errors");
16
+ const APPROVAL_TOKENS = new Set(["usdc", "ctf"]);
17
+ const WITHDRAW_ACTIONS = new Set(["request", "claim", "cancel"]);
18
+ const USDC_SCALE = 1000000n;
19
+ function isDecimalTokenId(token) {
20
+ return /^[0-9]+$/.test(token);
21
+ }
22
+ function normalizeApprovalToken(token) {
23
+ if (typeof token !== "string") {
24
+ throw new errors_1.ValidationError("token must be 'usdc', 'ctf', or a decimal CTF token_id string", "token");
25
+ }
26
+ const candidate = token.trim();
27
+ const normalized = candidate.toLowerCase();
28
+ if (APPROVAL_TOKENS.has(normalized)) {
29
+ return normalized;
30
+ }
31
+ if (isDecimalTokenId(candidate)) {
32
+ return candidate;
33
+ }
34
+ throw new errors_1.ValidationError("token must be 'usdc', 'ctf', or a decimal CTF token_id string", "token");
35
+ }
36
+ function normalizeAmountWei(amountWei) {
37
+ if (amountWei === undefined) {
38
+ return undefined;
39
+ }
40
+ if (typeof amountWei !== "bigint") {
41
+ throw new errors_1.ValidationError("amount_wei must be a non-negative integer", "amount_wei");
42
+ }
43
+ if (amountWei < 0n) {
44
+ throw new errors_1.ValidationError("amount_wei must be non-negative", "amount_wei");
45
+ }
46
+ return amountWei.toString();
47
+ }
48
+ function validateUsdcDecimal(value, field) {
49
+ const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(value);
50
+ if (!match) {
51
+ throw new errors_1.ValidationError(`${field} must be a finite positive number`, field);
52
+ }
53
+ const fractional = match[2] ?? "";
54
+ if (fractional.length > 6) {
55
+ throw new errors_1.ValidationError(`${field} precision exceeds 6 decimals; max precision for USDC is 0.000001`, field);
56
+ }
57
+ const scaled = BigInt(match[1]) * USDC_SCALE + BigInt(fractional.padEnd(6, "0"));
58
+ if (scaled <= 0n) {
59
+ throw new errors_1.ValidationError(`${field} must be positive`, field);
60
+ }
61
+ }
62
+ function normalizeUsdcAmount(value, field = "amount") {
63
+ if (typeof value === "bigint") {
64
+ if (value <= 0n) {
65
+ throw new errors_1.ValidationError(`${field} must be positive`, field);
66
+ }
67
+ return value.toString();
68
+ }
69
+ if (typeof value === "number") {
70
+ if (!Number.isFinite(value)) {
71
+ throw new errors_1.ValidationError(`${field} must be a finite positive number`, field);
72
+ }
73
+ validateUsdcDecimal(String(value), field);
74
+ return value;
75
+ }
76
+ if (typeof value === "string") {
77
+ const trimmed = value.trim();
78
+ validateUsdcDecimal(trimmed, field);
79
+ return trimmed;
80
+ }
81
+ throw new errors_1.ValidationError(`${field} must be a finite positive number`, field);
82
+ }
83
+ function normalizeWithdrawAction(action) {
84
+ if (typeof action === "string" && WITHDRAW_ACTIONS.has(action)) {
85
+ return action;
86
+ }
87
+ throw new errors_1.ValidationError("action must be 'request', 'claim', or 'cancel'", "action");
88
+ }
15
89
  class Escrow {
16
90
  client;
17
91
  constructor(client) {
@@ -22,13 +96,16 @@ class Escrow {
22
96
  * `amountWei` is omitted, the server returns an unlimited approval.
23
97
  */
24
98
  async approveTx(token, amountWei) {
99
+ const address = (0, hosted_routing_1.resolveWalletAddress)(this.client);
100
+ const approvalAmount = normalizeAmountWei(amountWei);
25
101
  const route = hosted_routing_1.HOSTED_METHOD_ROUTES.get("escrowApproveTx");
26
102
  return (0, hosted_routing_1._tradingRequest)(this.client, {
27
103
  method: route.method,
28
104
  path: route.path,
29
105
  body: {
30
- token,
31
- amount_wei: amountWei === undefined ? null : amountWei.toString(),
106
+ token: normalizeApprovalToken(token),
107
+ user_address: address,
108
+ ...(approvalAmount === undefined ? {} : { amount_wei: approvalAmount }),
32
109
  },
33
110
  });
34
111
  }
@@ -37,12 +114,15 @@ class Escrow {
37
114
  * grid). Accepts number, decimal string, or BigInt in micro-units.
38
115
  */
39
116
  async depositTx(amount) {
117
+ const address = (0, hosted_routing_1.resolveWalletAddress)(this.client);
40
118
  const route = hosted_routing_1.HOSTED_METHOD_ROUTES.get("escrowDepositTx");
41
119
  return (0, hosted_routing_1._tradingRequest)(this.client, {
42
120
  method: route.method,
43
121
  path: route.path,
44
122
  body: {
45
- amount: typeof amount === "bigint" ? amount.toString() : amount,
123
+ token: "usdc",
124
+ amount: normalizeUsdcAmount(amount),
125
+ user_address: address,
46
126
  },
47
127
  });
48
128
  }
@@ -52,16 +132,31 @@ class Escrow {
52
132
  * the timelock, `cancel` aborts a pending request.
53
133
  */
54
134
  async withdrawTx(action, amount) {
135
+ const normalizedAction = normalizeWithdrawAction(action);
136
+ const address = (0, hosted_routing_1.resolveWalletAddress)(this.client);
55
137
  const route = hosted_routing_1.HOSTED_METHOD_ROUTES.get("escrowWithdrawTx");
56
- const normalizedAmount = amount === undefined
57
- ? null
58
- : typeof amount === "bigint"
59
- ? amount.toString()
60
- : amount;
138
+ if (normalizedAction === "request") {
139
+ if (amount === undefined) {
140
+ throw new errors_1.ValidationError("amount is required when action='request'", "amount");
141
+ }
142
+ return (0, hosted_routing_1._tradingRequest)(this.client, {
143
+ method: route.method,
144
+ path: route.path,
145
+ body: {
146
+ action: normalizedAction,
147
+ token: "usdc",
148
+ amount: normalizeUsdcAmount(amount),
149
+ user_address: address,
150
+ },
151
+ });
152
+ }
153
+ if (amount !== undefined) {
154
+ throw new errors_1.ValidationError(`amount must be omitted when action='${normalizedAction}'`, "amount");
155
+ }
61
156
  return (0, hosted_routing_1._tradingRequest)(this.client, {
62
157
  method: route.method,
63
158
  path: route.path,
64
- body: { action, amount: normalizedAmount },
159
+ body: { action: normalizedAction, token: "usdc", user_address: address },
65
160
  });
66
161
  }
67
162
  /**
@@ -70,12 +165,16 @@ class Escrow {
70
165
  */
71
166
  async withdrawals(opts = {}) {
72
167
  const address = (0, hosted_routing_1.resolveWalletAddress)(this.client, opts.address);
168
+ const include = (opts.include ?? "pending,events").trim();
169
+ if (!include) {
170
+ throw new errors_1.ValidationError("include must not be empty", "include");
171
+ }
73
172
  const route = hosted_routing_1.HOSTED_METHOD_ROUTES.get("escrowWithdrawals");
74
173
  const path = (0, hosted_routing_1.formatRoutePath)(route, { address });
75
174
  return (0, hosted_routing_1._tradingRequest)(this.client, {
76
175
  method: route.method,
77
176
  path,
78
- params: { include: opts.include ?? "pending,events" },
177
+ params: { include },
79
178
  });
80
179
  }
81
180
  }
@@ -117,12 +117,14 @@ class ServerManager {
117
117
  * Check if the server is running.
118
118
  */
119
119
  async isServerRunning() {
120
- // Read lock file to get current port
121
- const port = this.getRunningPort();
120
+ const info = this.getServerInfo();
121
+ if (!info?.port) {
122
+ return false;
123
+ }
122
124
  try {
123
125
  // Use native fetch to check health on the actual running port
124
126
  // This avoids issues where this.api is configured with the wrong port
125
- const response = await fetch(`http://localhost:${port}/health`, {
127
+ const response = await fetch(`http://localhost:${info.port}/health`, {
126
128
  signal: AbortSignal.timeout(5_000),
127
129
  });
128
130
  if (response.ok) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.51.1",
3
+ "version": "2.51.2",
4
4
  "description": "OpenAPI client for pmxtjs",
5
5
  "author": "OpenAPI-Generator",
6
6
  "repository": {
package/index.ts CHANGED
@@ -19,14 +19,14 @@
19
19
  */
20
20
 
21
21
 
22
- import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock } from "./pmxt/client.js";
22
+ import { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock } from "./pmxt/client.js";
23
23
  import { Router } from "./pmxt/router.js";
24
24
  import { ServerManager } from "./pmxt/server-manager.js";
25
25
  import { FeedClient } from "./pmxt/feed-client.js";
26
26
  import * as models from "./pmxt/models.js";
27
27
  import * as errors from "./pmxt/errors.js";
28
28
 
29
- export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Mock, PolymarketOptions } from "./pmxt/client.js";
29
+ export { Exchange, Polymarket, Kalshi, KalshiDemo, Limitless, Myriad, Probable, Baozi, Opinion, Metaculus, Smarkets, PolymarketUS, GeminiTitan, Hyperliquid, SuiBets, Suibets, Rain, Hunch, Mock, PolymarketOptions } from "./pmxt/client.js";
30
30
  export { FeedClient } from "./pmxt/feed-client.js";
31
31
  export type { Ticker, Tickers, OHLCV, Market as FeedMarket, OracleRound, FeedClientOptions } from "./pmxt/feed-client.js";
32
32
  export { Router } from "./pmxt/router.js";
@@ -98,6 +98,7 @@ const pmxt = {
98
98
  SuiBets,
99
99
  Suibets,
100
100
  Rain,
101
+ Hunch,
101
102
  Mock,
102
103
  Router,
103
104
  ServerManager,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.51.1",
3
+ "version": "2.51.2",
4
4
  "description": "Unified prediction market data API - The ccxt for prediction markets",
5
5
  "author": "PMXT Contributors",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "unified"
44
44
  ],
45
45
  "dependencies": {
46
- "pmxt-core": "2.51.1",
46
+ "pmxt-core": "2.51.2",
47
47
  "ws": "^8.18.0"
48
48
  },
49
49
  "peerDependencies": {
package/pmxt/client.ts CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  import { ServerManager } from "./server-manager.js";
53
53
  import { buildArgsWithOptionalOptions } from "./args.js";
54
54
  import { PmxtError, fromServerError, InvalidOrder, NotSupported } from "./errors.js";
55
- import { LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js";
55
+ import { ENV, LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js";
56
56
  import { SidecarWsClient } from "./ws-client.js";
57
57
  import { logger } from "./logger.js";
58
58
 
@@ -351,6 +351,7 @@ export abstract class Exchange {
351
351
  private _wsClient: SidecarWsClient | null = null;
352
352
  /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
353
353
  private _wsUnsupported: boolean = false;
354
+ private readonly _useSidecarLockBaseUrl: boolean;
354
355
 
355
356
  constructor(exchangeName: string, options: ExchangeOptions = {}) {
356
357
  this.exchangeName = exchangeName.toLowerCase();
@@ -370,6 +371,11 @@ export abstract class Exchange {
370
371
  const baseUrl = resolved.baseUrl;
371
372
  this.pmxtApiKey = resolved.pmxtApiKey;
372
373
  this.isHosted = resolved.isHosted;
374
+ const hasBaseUrlOverride = Boolean(
375
+ options.baseUrl ||
376
+ (typeof process !== "undefined" && process.env[ENV.BASE_URL]),
377
+ );
378
+ this._useSidecarLockBaseUrl = !this.isHosted && !hasBaseUrlOverride;
373
379
 
374
380
  // Hosted trading bridge: if the caller passed a privateKey but no
375
381
  // explicit signer, lazily wrap it in an EthersSigner so that
@@ -417,16 +423,18 @@ export abstract class Exchange {
417
423
  try {
418
424
  await this.serverManager.ensureServerRunning();
419
425
 
420
- // Get the actual port the server is running on
421
- // (may differ from default if default port was busy)
422
- const actualPort = this.serverManager.getRunningPort();
423
- const newBaseUrl = `http://localhost:${actualPort}`;
426
+ if (this._useSidecarLockBaseUrl) {
427
+ // Get the actual port the server is running on
428
+ // (may differ from default if default port was busy)
429
+ const actualPort = this.serverManager.getRunningPort();
430
+ const newBaseUrl = `http://localhost:${actualPort}`;
424
431
 
425
- // Update API client with actual base URL
426
- this.config = new Configuration({
427
- basePath: newBaseUrl,
428
- });
429
- this.api = new DefaultApi(this.config);
432
+ // Update API client with actual base URL
433
+ this.config = new Configuration({
434
+ basePath: newBaseUrl,
435
+ });
436
+ this.api = new DefaultApi(this.config);
437
+ }
430
438
  } catch (error) {
431
439
  const msg =
432
440
  `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
@@ -511,12 +519,12 @@ export abstract class Exchange {
511
519
  /**
512
520
  * Resolve the current sidecar base URL.
513
521
  *
514
- * For hosted mode the configured basePath is returned as-is.
515
- * For local mode the port is re-read from the lock file on every
516
- * call so we pick up sidecar restarts that land on a different port.
522
+ * Hosted mode and explicit baseUrl clients keep the configured basePath.
523
+ * Default local clients re-read the lock-file port on every call so they
524
+ * pick up sidecar restarts that land on a different port.
517
525
  */
518
526
  private resolveBaseUrl(): string {
519
- if (this.isHosted) return this.config.basePath;
527
+ if (this.isHosted || !this._useSidecarLockBaseUrl) return this.config.basePath;
520
528
  const port = this.serverManager.getRunningPort();
521
529
  return `http://localhost:${port}`;
522
530
  }
@@ -1225,7 +1233,10 @@ export abstract class Exchange {
1225
1233
  const route = HOSTED_METHOD_ROUTES.get("fetchOrder")!;
1226
1234
  const path = formatRoutePath(route, { order_id: orderId });
1227
1235
  const data = await _tradingRequest(this, { method: route.method, path });
1228
- return orderFromV0(data as Record<string, unknown>);
1236
+ const payload = data && typeof data === "object" && "order" in data
1237
+ ? (data as { order: unknown }).order
1238
+ : data;
1239
+ return orderFromV0(payload as Record<string, unknown>);
1229
1240
  }
1230
1241
  try {
1231
1242
  const args: any[] = [];
@@ -1325,6 +1336,9 @@ export abstract class Exchange {
1325
1336
 
1326
1337
  async fetchClosedOrders(params?: OrderHistoryParams): Promise<Order[]> {
1327
1338
  await this.initPromise;
1339
+ if (this.isHosted) {
1340
+ throw new NotSupported("fetchClosedOrders is not available in hosted trading mode.");
1341
+ }
1328
1342
  try {
1329
1343
  const args: any[] = [];
1330
1344
  if (params !== undefined) args.push(params);
@@ -1351,6 +1365,9 @@ export abstract class Exchange {
1351
1365
 
1352
1366
  async fetchAllOrders(params?: OrderHistoryParams): Promise<Order[]> {
1353
1367
  await this.initPromise;
1368
+ if (this.isHosted) {
1369
+ throw new NotSupported("fetchAllOrders is not available in hosted trading mode.");
1370
+ }
1354
1371
  try {
1355
1372
  const args: any[] = [];
1356
1373
  if (params !== undefined) args.push(params);
@@ -1382,7 +1399,9 @@ export abstract class Exchange {
1382
1399
  const route = HOSTED_METHOD_ROUTES.get("fetchPositions")!;
1383
1400
  const path = formatRoutePath(route, { address: resolvedAddress });
1384
1401
  const data = await _tradingRequest(this, { method: route.method, path });
1385
- const list = Array.isArray(data) ? data : [];
1402
+ const list: unknown[] = Array.isArray(data)
1403
+ ? data
1404
+ : (data && Array.isArray((data as any).positions) ? (data as any).positions : []);
1386
1405
  return list.map((p) => positionFromV0(p as Record<string, unknown>));
1387
1406
  }
1388
1407
  try {
@@ -1416,7 +1435,9 @@ export abstract class Exchange {
1416
1435
  const route = HOSTED_METHOD_ROUTES.get("fetchBalance")!;
1417
1436
  const path = formatRoutePath(route, { address: resolvedAddress });
1418
1437
  const data = await _tradingRequest(this, { method: route.method, path });
1419
- const list = Array.isArray(data) ? data : [];
1438
+ const list: unknown[] = Array.isArray(data)
1439
+ ? data
1440
+ : (data && Array.isArray((data as any).balances) ? (data as any).balances : []);
1420
1441
  return list.map((b) => balanceFromV0(b as Record<string, unknown>));
1421
1442
  }
1422
1443
  try {
@@ -3048,6 +3069,16 @@ export interface PolymarketOptions {
3048
3069
 
3049
3070
  /** Optional signature type */
3050
3071
  signatureType?: 'eoa' | 'poly-proxy' | 'gnosis-safe' | number;
3072
+
3073
+ /**
3074
+ * EVM wallet address used for hosted reads/writes.
3075
+ */
3076
+ walletAddress?: string;
3077
+
3078
+ /**
3079
+ * External signer used for hosted writes.
3080
+ */
3081
+ signer?: Signer;
3051
3082
  }
3052
3083
 
3053
3084
  export class Polymarket extends Exchange {
@@ -3402,6 +3433,19 @@ export class Rain extends Exchange {
3402
3433
  }
3403
3434
  }
3404
3435
 
3436
+
3437
+ /**
3438
+ * Hunch exchange client.
3439
+ *
3440
+ * Hunch is a crypto-native prediction market. Reads are unauthenticated;
3441
+ * trading requires an EVM private key.
3442
+ */
3443
+ export class Hunch extends Exchange {
3444
+ constructor(options: ExchangeOptions = {}) {
3445
+ super("hunch", options);
3446
+ }
3447
+ }
3448
+
3405
3449
  /**
3406
3450
  * Mock exchange client.
3407
3451
  *
package/pmxt/escrow.ts CHANGED
@@ -16,6 +16,99 @@ import {
16
16
  HostedClientLike,
17
17
  resolveWalletAddress,
18
18
  } from "./hosted-routing";
19
+ import { ValidationError } from "./errors";
20
+
21
+ const APPROVAL_TOKENS = new Set(["usdc", "ctf"]);
22
+ const WITHDRAW_ACTIONS = new Set(["request", "claim", "cancel"]);
23
+ const USDC_SCALE = 1_000_000n;
24
+
25
+ function isDecimalTokenId(token: string): boolean {
26
+ return /^[0-9]+$/.test(token);
27
+ }
28
+
29
+ function normalizeApprovalToken(token: string): string {
30
+ if (typeof token !== "string") {
31
+ throw new ValidationError(
32
+ "token must be 'usdc', 'ctf', or a decimal CTF token_id string",
33
+ "token",
34
+ );
35
+ }
36
+
37
+ const candidate = token.trim();
38
+ const normalized = candidate.toLowerCase();
39
+ if (APPROVAL_TOKENS.has(normalized)) {
40
+ return normalized;
41
+ }
42
+ if (isDecimalTokenId(candidate)) {
43
+ return candidate;
44
+ }
45
+ throw new ValidationError(
46
+ "token must be 'usdc', 'ctf', or a decimal CTF token_id string",
47
+ "token",
48
+ );
49
+ }
50
+
51
+ function normalizeAmountWei(amountWei: bigint | undefined): string | undefined {
52
+ if (amountWei === undefined) {
53
+ return undefined;
54
+ }
55
+ if (typeof amountWei !== "bigint") {
56
+ throw new ValidationError("amount_wei must be a non-negative integer", "amount_wei");
57
+ }
58
+ if (amountWei < 0n) {
59
+ throw new ValidationError("amount_wei must be non-negative", "amount_wei");
60
+ }
61
+ return amountWei.toString();
62
+ }
63
+
64
+ function validateUsdcDecimal(value: string, field: string): void {
65
+ const match = /^([0-9]+)(?:\.([0-9]+))?$/.exec(value);
66
+ if (!match) {
67
+ throw new ValidationError(`${field} must be a finite positive number`, field);
68
+ }
69
+
70
+ const fractional = match[2] ?? "";
71
+ if (fractional.length > 6) {
72
+ throw new ValidationError(
73
+ `${field} precision exceeds 6 decimals; max precision for USDC is 0.000001`,
74
+ field,
75
+ );
76
+ }
77
+
78
+ const scaled = BigInt(match[1]) * USDC_SCALE + BigInt(fractional.padEnd(6, "0"));
79
+ if (scaled <= 0n) {
80
+ throw new ValidationError(`${field} must be positive`, field);
81
+ }
82
+ }
83
+
84
+ function normalizeUsdcAmount(value: number | string | bigint, field: string = "amount"): number | string {
85
+ if (typeof value === "bigint") {
86
+ if (value <= 0n) {
87
+ throw new ValidationError(`${field} must be positive`, field);
88
+ }
89
+ return value.toString();
90
+ }
91
+ if (typeof value === "number") {
92
+ if (!Number.isFinite(value)) {
93
+ throw new ValidationError(`${field} must be a finite positive number`, field);
94
+ }
95
+ validateUsdcDecimal(String(value), field);
96
+ return value;
97
+ }
98
+ if (typeof value === "string") {
99
+ const trimmed = value.trim();
100
+ validateUsdcDecimal(trimmed, field);
101
+ return trimmed;
102
+ }
103
+ throw new ValidationError(`${field} must be a finite positive number`, field);
104
+ }
105
+
106
+ function normalizeWithdrawAction(action: string): "request" | "claim" | "cancel" {
107
+ if (typeof action === "string" && WITHDRAW_ACTIONS.has(action)) {
108
+ return action as "request" | "claim" | "cancel";
109
+ }
110
+ throw new ValidationError("action must be 'request', 'claim', or 'cancel'", "action");
111
+ }
19
112
 
20
113
  export class Escrow {
21
114
  constructor(private readonly client: HostedClientLike) {}
@@ -25,13 +118,16 @@ export class Escrow {
25
118
  * `amountWei` is omitted, the server returns an unlimited approval.
26
119
  */
27
120
  async approveTx(token: string, amountWei?: bigint): Promise<unknown> {
121
+ const address = resolveWalletAddress(this.client);
122
+ const approvalAmount = normalizeAmountWei(amountWei);
28
123
  const route = HOSTED_METHOD_ROUTES.get("escrowApproveTx")!;
29
124
  return _tradingRequest(this.client, {
30
125
  method: route.method,
31
126
  path: route.path,
32
127
  body: {
33
- token,
34
- amount_wei: amountWei === undefined ? null : amountWei.toString(),
128
+ token: normalizeApprovalToken(token),
129
+ user_address: address,
130
+ ...(approvalAmount === undefined ? {} : { amount_wei: approvalAmount }),
35
131
  },
36
132
  });
37
133
  }
@@ -41,12 +137,15 @@ export class Escrow {
41
137
  * grid). Accepts number, decimal string, or BigInt in micro-units.
42
138
  */
43
139
  async depositTx(amount: number | string | bigint): Promise<unknown> {
140
+ const address = resolveWalletAddress(this.client);
44
141
  const route = HOSTED_METHOD_ROUTES.get("escrowDepositTx")!;
45
142
  return _tradingRequest(this.client, {
46
143
  method: route.method,
47
144
  path: route.path,
48
145
  body: {
49
- amount: typeof amount === "bigint" ? amount.toString() : amount,
146
+ token: "usdc",
147
+ amount: normalizeUsdcAmount(amount),
148
+ user_address: address,
50
149
  },
51
150
  });
52
151
  }
@@ -60,17 +159,34 @@ export class Escrow {
60
159
  action: "request" | "claim" | "cancel",
61
160
  amount?: number | string | bigint,
62
161
  ): Promise<unknown> {
162
+ const normalizedAction = normalizeWithdrawAction(action);
163
+ const address = resolveWalletAddress(this.client);
63
164
  const route = HOSTED_METHOD_ROUTES.get("escrowWithdrawTx")!;
64
- const normalizedAmount =
65
- amount === undefined
66
- ? null
67
- : typeof amount === "bigint"
68
- ? amount.toString()
69
- : amount;
165
+ if (normalizedAction === "request") {
166
+ if (amount === undefined) {
167
+ throw new ValidationError("amount is required when action='request'", "amount");
168
+ }
169
+ return _tradingRequest(this.client, {
170
+ method: route.method,
171
+ path: route.path,
172
+ body: {
173
+ action: normalizedAction,
174
+ token: "usdc",
175
+ amount: normalizeUsdcAmount(amount),
176
+ user_address: address,
177
+ },
178
+ });
179
+ }
180
+ if (amount !== undefined) {
181
+ throw new ValidationError(
182
+ `amount must be omitted when action='${normalizedAction}'`,
183
+ "amount",
184
+ );
185
+ }
70
186
  return _tradingRequest(this.client, {
71
187
  method: route.method,
72
188
  path: route.path,
73
- body: { action, amount: normalizedAmount },
189
+ body: { action: normalizedAction, token: "usdc", user_address: address },
74
190
  });
75
191
  }
76
192
 
@@ -82,12 +198,16 @@ export class Escrow {
82
198
  opts: { include?: string; address?: string } = {},
83
199
  ): Promise<unknown> {
84
200
  const address = resolveWalletAddress(this.client, opts.address);
201
+ const include = (opts.include ?? "pending,events").trim();
202
+ if (!include) {
203
+ throw new ValidationError("include must not be empty", "include");
204
+ }
85
205
  const route = HOSTED_METHOD_ROUTES.get("escrowWithdrawals")!;
86
206
  const path = formatRoutePath(route, { address });
87
207
  return _tradingRequest(this.client, {
88
208
  method: route.method,
89
209
  path,
90
- params: { include: opts.include ?? "pending,events" },
210
+ params: { include },
91
211
  });
92
212
  }
93
213
  }
@@ -103,13 +103,15 @@ export class ServerManager {
103
103
  * Check if the server is running.
104
104
  */
105
105
  async isServerRunning(): Promise<boolean> {
106
- // Read lock file to get current port
107
- const port = this.getRunningPort();
106
+ const info = this.getServerInfo();
107
+ if (!info?.port) {
108
+ return false;
109
+ }
108
110
 
109
111
  try {
110
112
  // Use native fetch to check health on the actual running port
111
113
  // This avoids issues where this.api is configured with the wrong port
112
- const response = await fetch(`http://localhost:${port}/health`, {
114
+ const response = await fetch(`http://localhost:${info.port}/health`, {
113
115
  signal: AbortSignal.timeout(5_000),
114
116
  });
115
117
  if (response.ok) {