pmxt-core 2.22.2 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/BaseExchange.d.ts +2 -0
  2. package/dist/exchanges/kalshi/api.d.ts +1 -1
  3. package/dist/exchanges/kalshi/api.js +1 -1
  4. package/dist/exchanges/limitless/api.d.ts +1 -1
  5. package/dist/exchanges/limitless/api.js +1 -1
  6. package/dist/exchanges/metaculus/api.d.ts +212 -0
  7. package/dist/exchanges/metaculus/api.js +418 -0
  8. package/dist/exchanges/metaculus/cancelOrder.d.ts +38 -0
  9. package/dist/exchanges/metaculus/cancelOrder.js +74 -0
  10. package/dist/exchanges/metaculus/createOrder.d.ts +107 -0
  11. package/dist/exchanges/metaculus/createOrder.js +272 -0
  12. package/dist/exchanges/metaculus/errors.d.ts +21 -0
  13. package/dist/exchanges/metaculus/errors.js +59 -0
  14. package/dist/exchanges/metaculus/fetchEvents.d.ts +5 -0
  15. package/dist/exchanges/metaculus/fetchEvents.js +187 -0
  16. package/dist/exchanges/metaculus/fetchMarkets.d.ts +6 -0
  17. package/dist/exchanges/metaculus/fetchMarkets.js +198 -0
  18. package/dist/exchanges/metaculus/index.d.ts +105 -0
  19. package/dist/exchanges/metaculus/index.js +166 -0
  20. package/dist/exchanges/metaculus/utils.d.ts +40 -0
  21. package/dist/exchanges/metaculus/utils.js +320 -0
  22. package/dist/exchanges/myriad/api.d.ts +1 -1
  23. package/dist/exchanges/myriad/api.js +1 -1
  24. package/dist/exchanges/opinion/api.d.ts +1 -1
  25. package/dist/exchanges/opinion/api.js +1 -1
  26. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  27. package/dist/exchanges/polymarket/api-clob.js +1 -1
  28. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  29. package/dist/exchanges/polymarket/api-data.js +1 -1
  30. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  31. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  32. package/dist/exchanges/polymarket/auth.js +42 -8
  33. package/dist/exchanges/polymarket/index.js +56 -19
  34. package/dist/exchanges/probable/api.d.ts +1 -1
  35. package/dist/exchanges/probable/api.js +1 -1
  36. package/dist/exchanges/probable/auth.js +5 -2
  37. package/dist/exchanges/smarkets/api.d.ts +8067 -0
  38. package/dist/exchanges/smarkets/api.js +10698 -0
  39. package/dist/exchanges/smarkets/auth.d.ts +56 -0
  40. package/dist/exchanges/smarkets/auth.js +105 -0
  41. package/dist/exchanges/smarkets/config.d.ts +41 -0
  42. package/dist/exchanges/smarkets/config.js +47 -0
  43. package/dist/exchanges/smarkets/errors.d.ts +31 -0
  44. package/dist/exchanges/smarkets/errors.js +186 -0
  45. package/dist/exchanges/smarkets/fetcher.d.ts +177 -0
  46. package/dist/exchanges/smarkets/fetcher.js +342 -0
  47. package/dist/exchanges/smarkets/index.d.ts +54 -0
  48. package/dist/exchanges/smarkets/index.js +285 -0
  49. package/dist/exchanges/smarkets/normalizer.d.ts +18 -0
  50. package/dist/exchanges/smarkets/normalizer.js +267 -0
  51. package/dist/exchanges/smarkets/price.d.ts +26 -0
  52. package/dist/exchanges/smarkets/price.js +44 -0
  53. package/dist/exchanges/smarkets/price.test.d.ts +1 -0
  54. package/dist/exchanges/smarkets/price.test.js +50 -0
  55. package/dist/index.d.ts +8 -0
  56. package/dist/index.js +9 -1
  57. package/dist/server/app.js +22 -3
  58. package/package.json +3 -3
@@ -0,0 +1,40 @@
1
+ import { UnifiedMarket } from "../../types";
2
+ /**
3
+ * Base URL passed to parseOpenApiSpec to override the spec's servers[0].url.
4
+ * The generated api.ts already has "https://www.metaculus.com/api" as its server URL,
5
+ * so this constant must match exactly -- do NOT add a trailing slash or path suffix.
6
+ * Paths in the spec (/posts/, /posts/{postId}/) are appended directly by BaseExchange.
7
+ */
8
+ export declare const BASE_URL = "https://www.metaculus.com/api";
9
+ /**
10
+ * Map a Metaculus post `status` to pmxt unified status.
11
+ *
12
+ * Metaculus post statuses: "open", "closed", "resolved", "upcoming"
13
+ */
14
+ export declare function mapStatus(status: string): "active" | "closed";
15
+ /**
16
+ * Convert a raw Metaculus Post (v3 /api/posts/ response item) into a
17
+ * `UnifiedMarket`.
18
+ *
19
+ * Returns `null` for group-of-questions posts -- callers should use
20
+ * {@link expandPost} instead, which handles both single and group posts.
21
+ *
22
+ * @param post Raw post object from the Metaculus API.
23
+ * @param eventId Optional parent event ID (tournament slug) to override
24
+ * the value derived from post.projects.tournament.
25
+ */
26
+ export declare function mapMarketToUnified(post: any, eventId?: string): UnifiedMarket | null;
27
+ /**
28
+ * Convert a raw Metaculus post into one or more `UnifiedMarket` objects.
29
+ *
30
+ * Handles all post types:
31
+ * - Single-question posts (binary, multiple-choice, continuous) -> 1 market
32
+ * - Group-of-questions posts -> N markets (one per sub-question)
33
+ *
34
+ * Use this instead of calling `mapMarketToUnified` directly when processing
35
+ * feed results, since a single API post can yield multiple tradeable markets.
36
+ *
37
+ * @param post Raw post object from the Metaculus API.
38
+ * @param eventId Optional parent event ID (tournament slug).
39
+ */
40
+ export declare function expandPost(post: any, eventId?: string): UnifiedMarket[];
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASE_URL = void 0;
4
+ exports.mapStatus = mapStatus;
5
+ exports.mapMarketToUnified = mapMarketToUnified;
6
+ exports.expandPost = expandPost;
7
+ const market_utils_1 = require("../../utils/market-utils");
8
+ /**
9
+ * Base URL passed to parseOpenApiSpec to override the spec's servers[0].url.
10
+ * The generated api.ts already has "https://www.metaculus.com/api" as its server URL,
11
+ * so this constant must match exactly -- do NOT add a trailing slash or path suffix.
12
+ * Paths in the spec (/posts/, /posts/{postId}/) are appended directly by BaseExchange.
13
+ */
14
+ exports.BASE_URL = "https://www.metaculus.com/api";
15
+ /**
16
+ * Map a Metaculus post `status` to pmxt unified status.
17
+ *
18
+ * Metaculus post statuses: "open", "closed", "resolved", "upcoming"
19
+ */
20
+ function mapStatus(status) {
21
+ switch ((status ?? "").toLowerCase()) {
22
+ case "open":
23
+ case "upcoming":
24
+ return "active";
25
+ default:
26
+ return "closed";
27
+ }
28
+ }
29
+ /**
30
+ * Extract the community prediction probability from a question object.
31
+ *
32
+ * For all question types the recency-weighted aggregation exposes a `centers`
33
+ * array where `centers[0]` is the median / central estimate, already normalised
34
+ * to [0, 1] by the API.
35
+ *
36
+ * Accepts either a Post (reads from post.question) or a bare Question object.
37
+ *
38
+ * @returns A number in [0, 1], or 0.5 if no prediction is available.
39
+ */
40
+ function extractCommunityProbability(questionOrPost) {
41
+ // Support both post.question and bare question objects
42
+ const question = questionOrPost?.question ?? questionOrPost;
43
+ const latest = question?.aggregations?.recency_weighted?.latest;
44
+ if (!latest)
45
+ return 0.5;
46
+ const centers = latest.centers;
47
+ if (Array.isArray(centers) && centers.length > 0 && typeof centers[0] === "number") {
48
+ return Math.max(0, Math.min(1, centers[0]));
49
+ }
50
+ // Fallback: some binary posts expose forecast_values[0] as the Yes probability
51
+ const fv = latest.forecast_values;
52
+ if (Array.isArray(fv) && fv.length > 0 && typeof fv[0] === "number") {
53
+ return Math.max(0, Math.min(1, fv[0]));
54
+ }
55
+ return 0.5;
56
+ }
57
+ /**
58
+ * Build the tag list from a Post's project associations.
59
+ * Combines taxonomy tags and categories so consumers can filter by either.
60
+ */
61
+ function buildTags(post) {
62
+ const tags = [];
63
+ const projects = post?.projects ?? {};
64
+ // Explicit tags
65
+ const tagList = projects.tag ?? [];
66
+ for (const t of tagList) {
67
+ const label = typeof t === "string" ? t : t?.name;
68
+ if (label && !tags.includes(label))
69
+ tags.push(label);
70
+ }
71
+ // Categories (useful for broad filtering)
72
+ const catList = projects.category ?? [];
73
+ for (const c of catList) {
74
+ const label = typeof c === "string" ? c : c?.name;
75
+ if (label && !tags.includes(label))
76
+ tags.push(label);
77
+ }
78
+ // Question type as a tag for easy filtering
79
+ const qType = post?.question?.type;
80
+ if (qType && !tags.includes(qType))
81
+ tags.push(qType);
82
+ return tags;
83
+ }
84
+ /**
85
+ * Build outcomes for a Metaculus question.
86
+ *
87
+ * OutcomeId format uses the **question ID** (not the post ID) so that
88
+ * `createOrder` can extract the correct ID for the forecast API.
89
+ *
90
+ * - Binary: `<questionId>-YES` / `<questionId>-NO`
91
+ * - Multiple-choice: `<questionId>-<categoryIndex>`
92
+ * - Continuous: `<questionId>-HIGHER` / `<questionId>-LOWER` (read-only, not tradeable)
93
+ *
94
+ * Raw aggregation data is exposed in each outcome's `metadata` so consumers
95
+ * can use it directly.
96
+ *
97
+ * @param question The Metaculus Question object (not the Post wrapper).
98
+ * @param postId The parent post ID, used as the marketId on each outcome.
99
+ */
100
+ function buildOutcomes(question, postId, medianProb) {
101
+ const questionId = String(question?.id ?? postId);
102
+ const type = (question?.type || "binary").toLowerCase();
103
+ const latest = question?.aggregations?.recency_weighted?.latest ?? null;
104
+ const sharedMeta = {
105
+ question_type: type,
106
+ question_id: Number(questionId),
107
+ aggregations: latest,
108
+ resolution: question?.resolution ?? null,
109
+ scaling: question?.scaling ?? null,
110
+ possibilities: question?.possibilities ?? null,
111
+ };
112
+ // Multiple choice: one outcome per option, each independently forecastable
113
+ if (type === "multiple_choice") {
114
+ const options = question?.options ?? [];
115
+ if (options.length > 0) {
116
+ const histogram = latest?.histogram ?? undefined;
117
+ return options.map((opt, idx) => {
118
+ const label = typeof opt === "string"
119
+ ? opt
120
+ : opt?.label ?? opt?.value ?? `Option ${idx + 1}`;
121
+ const price = Array.isArray(histogram) && typeof histogram[idx] === "number"
122
+ ? Math.max(0, Math.min(1, histogram[idx]))
123
+ : 1 / Math.max(options.length, 1);
124
+ return {
125
+ outcomeId: `${questionId}-${idx}`,
126
+ marketId: postId,
127
+ label,
128
+ price,
129
+ priceChange24h: 0,
130
+ metadata: { ...sharedMeta, choice_index: idx },
131
+ };
132
+ });
133
+ }
134
+ }
135
+ // Binary: Yes/No outcomes
136
+ if (type === "binary") {
137
+ return [
138
+ {
139
+ outcomeId: `${questionId}-YES`,
140
+ marketId: postId,
141
+ label: "Yes",
142
+ price: medianProb,
143
+ priceChange24h: 0,
144
+ metadata: sharedMeta,
145
+ },
146
+ {
147
+ outcomeId: `${questionId}-NO`,
148
+ marketId: postId,
149
+ label: "No",
150
+ price: Math.max(0, Math.min(1, 1 - medianProb)),
151
+ priceChange24h: 0,
152
+ metadata: sharedMeta,
153
+ },
154
+ ];
155
+ }
156
+ // Continuous / numeric / date -- not tradeable via createOrder.
157
+ // Displayed as synthetic Higher/Lower for read-only price indication.
158
+ return [
159
+ {
160
+ outcomeId: `${questionId}-HIGHER`,
161
+ marketId: postId,
162
+ label: "Higher",
163
+ price: medianProb,
164
+ priceChange24h: 0,
165
+ metadata: sharedMeta,
166
+ },
167
+ {
168
+ outcomeId: `${questionId}-LOWER`,
169
+ marketId: postId,
170
+ label: "Lower",
171
+ price: Math.max(0, Math.min(1, 1 - medianProb)),
172
+ priceChange24h: 0,
173
+ metadata: sharedMeta,
174
+ },
175
+ ];
176
+ }
177
+ /**
178
+ * Convert a raw Metaculus Post (v3 /api/posts/ response item) into a
179
+ * `UnifiedMarket`.
180
+ *
181
+ * Returns `null` for group-of-questions posts -- callers should use
182
+ * {@link expandPost} instead, which handles both single and group posts.
183
+ *
184
+ * @param post Raw post object from the Metaculus API.
185
+ * @param eventId Optional parent event ID (tournament slug) to override
186
+ * the value derived from post.projects.tournament.
187
+ */
188
+ function mapMarketToUnified(post, eventId) {
189
+ if (!post || !post.id)
190
+ return null;
191
+ // Group-of-questions posts have no top-level question -- they must be
192
+ // expanded into individual sub-question markets via expandPost().
193
+ if (post.group_of_questions && !post.question)
194
+ return null;
195
+ const postId = String(post.id);
196
+ const question = post.question ?? {};
197
+ const medianProb = extractCommunityProbability(post);
198
+ const outcomes = buildOutcomes(question, postId, medianProb);
199
+ // Resolution date -- prefer scheduled_resolve_time, fall back to close time
200
+ const resolveDateStr = post.scheduled_resolve_time ??
201
+ question.scheduled_resolve_time ??
202
+ post.scheduled_close_time ??
203
+ post.actual_close_time;
204
+ const resolutionDate = resolveDateStr
205
+ ? new Date(resolveDateStr)
206
+ : new Date("2099-01-01T00:00:00Z");
207
+ const tags = buildTags(post);
208
+ // Primary category label
209
+ const categoryList = post?.projects?.category ?? [];
210
+ const category = categoryList.length > 0
211
+ ? typeof categoryList[0] === "string"
212
+ ? categoryList[0]
213
+ : categoryList[0]?.name
214
+ : undefined;
215
+ // Forecaster count -- proxy for liquidity (no monetary values on Metaculus)
216
+ const forecastCount = Number(post.nr_forecasters ?? question.nr_forecasters ?? 0);
217
+ // Derive eventId from first tournament slug if not explicitly provided
218
+ const tournamentList = post?.projects?.tournament ?? [];
219
+ const derivedEventId = tournamentList.length > 0
220
+ ? typeof tournamentList[0] === "string"
221
+ ? tournamentList[0]
222
+ : tournamentList[0]?.slug
223
+ : undefined;
224
+ const resolvedEventId = eventId ?? derivedEventId;
225
+ const um = {
226
+ marketId: postId,
227
+ eventId: resolvedEventId,
228
+ title: post.title ?? question.title ?? "",
229
+ description: question.description ??
230
+ question.resolution_criteria ??
231
+ "",
232
+ slug: post.slug ?? post.url_title ?? undefined,
233
+ outcomes,
234
+ resolutionDate,
235
+ volume24h: 0, // Metaculus has no monetary volume
236
+ volume: 0,
237
+ liquidity: forecastCount, // re-purposed as forecaster count
238
+ openInterest: forecastCount,
239
+ url: `https://www.metaculus.com/questions/${postId}/`,
240
+ image: post.projects?.default_project?.header_image ?? undefined,
241
+ category,
242
+ tags,
243
+ };
244
+ (0, market_utils_1.addBinaryOutcomes)(um);
245
+ return um;
246
+ }
247
+ /**
248
+ * Expand a group-of-questions post into individual sub-question markets.
249
+ *
250
+ * Each sub-question becomes its own `UnifiedMarket` with:
251
+ * - `marketId` = sub-question's post_id (for API lookups via GetPost)
252
+ * - outcomeIds based on the sub-question's question.id (for forecast API)
253
+ * - `eventId` = parent post ID (the group acts as a container)
254
+ * - `metadata.groupPostId` on each outcome for traceability
255
+ *
256
+ * @param post A group-of-questions post (post.group_of_questions.questions[]).
257
+ * @param eventId Optional override for the eventId field.
258
+ */
259
+ function mapGroupPostToMarkets(post, eventId) {
260
+ const group = post.group_of_questions;
261
+ if (!group?.questions?.length)
262
+ return [];
263
+ const parentPostId = String(post.id);
264
+ const groupEventId = eventId ?? parentPostId;
265
+ const markets = [];
266
+ for (const subQuestion of group.questions) {
267
+ // Build a synthetic post that mapMarketToUnified can process.
268
+ // Use the sub-question's post_id as the post id if available,
269
+ // otherwise fall back to the sub-question's own id.
270
+ const syntheticPost = {
271
+ id: subQuestion.post_id ?? subQuestion.id,
272
+ title: subQuestion.title ?? subQuestion.label ?? post.title,
273
+ question: subQuestion,
274
+ // Inherit metadata from the parent post
275
+ slug: post.slug,
276
+ url_title: post.url_title,
277
+ projects: post.projects,
278
+ nr_forecasters: subQuestion.nr_forecasters ?? post.nr_forecasters,
279
+ scheduled_resolve_time: subQuestion.scheduled_resolve_time ?? post.scheduled_resolve_time,
280
+ scheduled_close_time: subQuestion.scheduled_close_time ?? post.scheduled_close_time,
281
+ actual_close_time: subQuestion.actual_close_time ?? post.actual_close_time,
282
+ status: post.status,
283
+ };
284
+ const market = mapMarketToUnified(syntheticPost, groupEventId);
285
+ if (market) {
286
+ // Tag each outcome with the parent group post ID for traceability
287
+ for (const outcome of market.outcomes) {
288
+ if (outcome.metadata) {
289
+ outcome.metadata.groupPostId = Number(parentPostId);
290
+ }
291
+ }
292
+ markets.push(market);
293
+ }
294
+ }
295
+ return markets;
296
+ }
297
+ /**
298
+ * Convert a raw Metaculus post into one or more `UnifiedMarket` objects.
299
+ *
300
+ * Handles all post types:
301
+ * - Single-question posts (binary, multiple-choice, continuous) -> 1 market
302
+ * - Group-of-questions posts -> N markets (one per sub-question)
303
+ *
304
+ * Use this instead of calling `mapMarketToUnified` directly when processing
305
+ * feed results, since a single API post can yield multiple tradeable markets.
306
+ *
307
+ * @param post Raw post object from the Metaculus API.
308
+ * @param eventId Optional parent event ID (tournament slug).
309
+ */
310
+ function expandPost(post, eventId) {
311
+ if (!post || !post.id)
312
+ return [];
313
+ // Group posts: expand each sub-question into its own market
314
+ if (post.group_of_questions && !post.question) {
315
+ return mapGroupPostToMarkets(post, eventId);
316
+ }
317
+ // Single-question post
318
+ const market = mapMarketToUnified(post, eventId);
319
+ return market ? [market] : [];
320
+ }
@@ -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-02T13:41:49.135Z
3
+ * Generated at: 2026-04-06T14:01:00.996Z
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-02T13:41:49.135Z
6
+ * Generated at: 2026-04-06T14:01:00.996Z
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-02T13:41:49.140Z
3
+ * Generated at: 2026-04-06T14:01:01.002Z
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-02T13:41:49.140Z
6
+ * Generated at: 2026-04-06T14:01:01.002Z
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-02T13:41:49.090Z
3
+ * Generated at: 2026-04-06T14:01:00.943Z
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-02T13:41:49.090Z
6
+ * Generated at: 2026-04-06T14:01:00.943Z
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-02T13:41:49.104Z
3
+ * Generated at: 2026-04-06T14:01:00.959Z
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-02T13:41:49.104Z
6
+ * Generated at: 2026-04-06T14:01:00.959Z
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-02T13:41:49.101Z
3
+ * Generated at: 2026-04-06T14:01:00.956Z
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-02T13:41:49.101Z
6
+ * Generated at: 2026-04-06T14:01:00.956Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.polymarketGammaSpec = {
@@ -158,22 +158,56 @@ class PolymarketAuth {
158
158
  if (this.clobClient) {
159
159
  return this.clobClient;
160
160
  }
161
- // 1. Determine proxy and signature type early
161
+ // 1. Determine proxy and signature type early.
162
+ //
163
+ // Important: if signatureType is not provided we MUST run discovery
164
+ // even when funderAddress is provided. Previously this branch was
165
+ // skipped whenever funderAddress was set, which silently defaulted
166
+ // signatureType to 0 (EOA). For wallets whose funds live on a Gnosis
167
+ // Safe (the modern Polymarket onboarding default) the CLOB then
168
+ // reports balance "0" instead of the real value, with no error.
169
+ const sigTypeProvided = this.credentials.signatureType !== undefined && this.credentials.signatureType !== null;
162
170
  let proxyAddress = this.credentials.funderAddress || undefined;
163
- let signatureType = this.mapSignatureType(this.credentials.signatureType);
164
- if (!proxyAddress) {
165
- const discovered = await this.discoverProxy();
166
- proxyAddress = discovered.proxyAddress;
167
- if (this.credentials.signatureType === undefined || this.credentials.signatureType === null) {
168
- signatureType = discovered.signatureType;
171
+ let signatureType = sigTypeProvided
172
+ ? this.mapSignatureType(this.credentials.signatureType)
173
+ : undefined;
174
+ // Run discovery if either piece is missing. Note: discoverProxy()
175
+ // returns a synthetic { proxyAddress: signerEOA, signatureType: 0 }
176
+ // fallback when its HTTP call fails — that fallback should NOT be
177
+ // used to populate signatureType when funderAddress is already set,
178
+ // because it would silently assign EOA semantics to a Gnosis Safe.
179
+ let discoverySucceeded = false;
180
+ if (!proxyAddress || signatureType === undefined) {
181
+ try {
182
+ const discovered = await this.discoverProxy();
183
+ discoverySucceeded =
184
+ !!this.discoveredProxyAddress &&
185
+ this.discoveredSignatureType !== undefined;
186
+ if (!proxyAddress) {
187
+ proxyAddress = discovered.proxyAddress;
188
+ }
189
+ if (signatureType === undefined && discoverySucceeded) {
190
+ signatureType = discovered.signatureType;
191
+ }
192
+ }
193
+ catch {
194
+ // Discovery failure is handled by the heuristic below.
169
195
  }
170
196
  }
171
197
  // Get API credentials (L1 auth)
172
- // Pass signature type if we know it (some accounts need it for derivation?)
173
198
  const apiCreds = await this.getApiCredentials();
174
199
  // 3. Defaults
175
200
  const signerAddress = this.signer.address;
176
201
  const finalProxyAddress = (proxyAddress || signerAddress);
202
+ // If signature type is still unknown, infer from address relationship:
203
+ // when the funder differs from the signer EOA, the funder must be a
204
+ // proxy/safe — default to Gnosis Safe (2), which is what Polymarket
205
+ // has created for new accounts since 2023. Users on the legacy
206
+ // Polymarket Proxy (1) need to set signatureType explicitly.
207
+ if (signatureType === undefined) {
208
+ signatureType =
209
+ finalProxyAddress.toLowerCase() !== signerAddress.toLowerCase() ? 2 : 0;
210
+ }
177
211
  const finalSignatureType = signatureType;
178
212
  // Create L2-authenticated client
179
213
  // console.log(`[PolymarketAuth] Initializing ClobClient | Signer: ${signerAddress} | Funder: ${finalProxyAddress} | SigType: ${finalSignatureType}`);
@@ -357,45 +357,82 @@ class PolymarketExchange extends BaseExchange_1.PredictionMarketExchange {
357
357
  const client = await auth.getClobClient();
358
358
  // Polymarket relies strictly on USDC (Polygon)
359
359
  const USDC_DECIMALS = 6;
360
- // Try fetching from CLOB client first
360
+ // Try fetching from CLOB client first.
361
+ //
362
+ // Note on the bundled @polymarket/clob-client error model: its HTTP
363
+ // wrapper swallows axios errors and RETURNS an envelope shaped like
364
+ // { error, status } instead of throwing. We must therefore validate
365
+ // the shape of the result before using it, otherwise downstream
366
+ // parseFloat() yields NaN and the on-chain fallback never triggers.
361
367
  let total = 0;
368
+ let clobBalanceAvailable = false;
362
369
  try {
363
370
  const balRes = await client.getBalanceAllowance({
364
371
  asset_type: clob_client_1.AssetType.COLLATERAL,
365
372
  });
366
- const rawBalance = parseFloat(balRes.balance);
367
- total = rawBalance / Math.pow(10, USDC_DECIMALS);
373
+ if (balRes && typeof balRes.balance === 'string') {
374
+ const rawBalance = parseFloat(balRes.balance);
375
+ if (Number.isFinite(rawBalance)) {
376
+ total = rawBalance / Math.pow(10, USDC_DECIMALS);
377
+ clobBalanceAvailable = true;
378
+ }
379
+ }
380
+ // If balRes was an error envelope, fall through to on-chain.
368
381
  }
369
382
  catch (clobError) {
370
- // If CLOB fails or returns 0 (suspiciously), we can try on-chain
371
- // but let's assume we proceed to on-chain check if total is 0
372
- // or just do on-chain check always for robustness if possible.
373
- // For now, let's trust CLOB but add On-Chain fallback if CLOB returns 0.
383
+ // Network/transport error fall through to on-chain.
374
384
  }
375
385
  // On-Chain Fallback/Check (Robustness)
376
- // If CLOB reported 0, let's verify on-chain because sometimes CLOB is behind or confused about proxies
377
- if (total === 0) {
386
+ // Trigger when CLOB couldn't tell us, or reported a true zero (CLOB
387
+ // can lag or be confused about proxies for newly funded wallets).
388
+ if (!clobBalanceAvailable || total === 0) {
378
389
  try {
379
390
  const targetAddress = await auth.getEffectiveFunderAddress();
380
391
  const balances = await this.getAddressOnChainBalance(targetAddress);
381
- total = balances[0]?.total ?? 0;
392
+ const onChain = balances[0]?.total ?? 0;
393
+ if (onChain > 0) {
394
+ total = onChain;
395
+ }
382
396
  }
383
397
  catch {
384
398
  }
385
399
  }
386
- // 2. Fetch open orders to calculate locked funds
387
- // We only care about BUY orders for USDC balance locking
388
- const openOrders = await client.getOpenOrders({});
400
+ // 2. Fetch open orders to calculate locked funds.
401
+ // We only care about BUY orders for USDC balance locking.
402
+ //
403
+ // The bundled @polymarket/clob-client throws "response.data is not
404
+ // iterable" from inside its getOpenOrders pagination loop whenever
405
+ // the CLOB API returns an HTTP error envelope (the library spreads
406
+ // response.data unconditionally). This is the root cause of #72:
407
+ // a wallet that has not completed Polymarket onboarding triggers an
408
+ // upstream auth/setup rejection that surfaces here as an opaque
409
+ // TypeError. Catch it and translate to a clear AuthenticationError.
389
410
  let locked = 0;
390
- if (openOrders && Array.isArray(openOrders)) {
391
- for (const order of openOrders) {
392
- if (order.side === clob_client_1.Side.BUY) {
393
- const remainingSize = parseFloat(order.original_size) - parseFloat(order.size_matched);
394
- const price = parseFloat(order.price);
395
- locked += remainingSize * price;
411
+ try {
412
+ const openOrders = await client.getOpenOrders({});
413
+ if (Array.isArray(openOrders)) {
414
+ for (const order of openOrders) {
415
+ if (order.side === clob_client_1.Side.BUY) {
416
+ const remainingSize = parseFloat(order.original_size) - parseFloat(order.size_matched);
417
+ const price = parseFloat(order.price);
418
+ locked += remainingSize * price;
419
+ }
396
420
  }
397
421
  }
398
422
  }
423
+ catch (ordersError) {
424
+ const msg = String(ordersError?.message ?? ordersError);
425
+ if (msg.includes('is not iterable')) {
426
+ throw new errors_1.AuthenticationError('Polymarket CLOB rejected the request to list open orders. ' +
427
+ 'This usually means the wallet has not completed Polymarket ' +
428
+ 'onboarding (no proxy/safe exists for this signer), or that ' +
429
+ 'funderAddress / signatureType need to be passed explicitly. ' +
430
+ 'Visit https://polymarket.com to complete account setup, or ' +
431
+ 'pass funderAddress and signatureType in credentials.', 'Polymarket');
432
+ }
433
+ // Unexpected failure — surface through the standard mapper.
434
+ throw ordersError;
435
+ }
399
436
  return [{
400
437
  currency: 'USDC',
401
438
  total: total,
@@ -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-02T13:41:49.127Z
3
+ * Generated at: 2026-04-06T14:01:00.988Z
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-02T13:41:49.127Z
6
+ * Generated at: 2026-04-06T14:01:00.988Z
7
7
  * Do not edit manually -- run "npm run fetch:openapi" to regenerate.
8
8
  */
9
9
  exports.probableApiSpec = {