pandora-cli-skills 1.1.55 → 1.1.57

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.
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pandora-cli-skills
3
3
  summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
4
- version: 1.1.55
4
+ version: 1.1.57
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -14,7 +14,7 @@ const ARB_MARKET_FIELDS = [
14
14
  ];
15
15
 
16
16
  const ARB_USAGE =
17
- 'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
17
+ 'pandora arb scan [--source pandora|polymarket] [--markets <csv>] --output ndjson|json [--limit <n>] [--min-net-spread-pct <n>|--min-spread-pct <n>] [--min-tvl <usdc>] [--fee-pct-per-leg <n>] [--slippage-pct-per-leg <n>] [--amount-usdc <n>] [--combinatorial] [--max-bundle-size <n>] [--similarity-threshold <0-1>] [--min-token-score <0-1>] [--max-close-diff-hours <n>] [--question-contains <text>] [--interval-ms <ms>] [--iterations <n>] [--indexer-url <url>] [--timeout-ms <ms>]';
18
18
 
19
19
  function requireDep(deps, name) {
20
20
  if (!deps || typeof deps[name] !== 'function') {
@@ -87,6 +87,12 @@ function buildMarketSnapshots(markets, orderedIds) {
87
87
  function buildCrossVenueArbOpportunities(payload, options) {
88
88
  const opportunities = Array.isArray(payload && payload.opportunities) ? payload.opportunities : [];
89
89
  const minSpreadPct = Number.isFinite(options.minNetSpreadPct) ? Number(options.minNetSpreadPct) : 0;
90
+ const feePctPerLeg = Number.isFinite(options.feePctPerLeg) ? Number(options.feePctPerLeg) : 0;
91
+ const slippagePctPerLeg = Number.isFinite(options.slippagePctPerLeg) ? Number(options.slippagePctPerLeg) : 0;
92
+ const feeImpactPct = roundNumber(Math.max(0, feePctPerLeg) * 2, 6);
93
+ const slippageImpactPct = roundNumber(Math.max(0, slippagePctPerLeg) * 2, 6);
94
+ const totalImpactPct = roundNumber(feeImpactPct + slippageImpactPct, 6);
95
+ const minTvlUsdc = Number.isFinite(options.minTvlUsdc) ? Number(options.minTvlUsdc) : 0;
90
96
  const rows = [];
91
97
  for (const item of opportunities) {
92
98
  const legs = Array.isArray(item.legs) ? item.legs : [];
@@ -96,7 +102,21 @@ function buildCrossVenueArbOpportunities(payload, options) {
96
102
 
97
103
  const spreadYesPct = toFiniteNumber(item.spreadYesPct);
98
104
  const spreadNoPct = toFiniteNumber(item.spreadNoPct);
99
- const netSpreadPct = Math.max(spreadYesPct === null ? 0 : spreadYesPct, spreadNoPct === null ? 0 : spreadNoPct);
105
+ const grossSpreadPct = Math.max(spreadYesPct === null ? 0 : spreadYesPct, spreadNoPct === null ? 0 : spreadNoPct);
106
+ const pandoraLiquidityUsd = toFiniteNumber(pandoraLeg.liquidityUsd);
107
+ const polymarketLiquidityUsd = toFiniteNumber(polyLeg.liquidityUsd);
108
+ const minLegLiquidityUsd =
109
+ pandoraLiquidityUsd !== null && polymarketLiquidityUsd !== null
110
+ ? Math.min(pandoraLiquidityUsd, polymarketLiquidityUsd)
111
+ : null;
112
+
113
+ if (minTvlUsdc > 0) {
114
+ if (minLegLiquidityUsd === null || minLegLiquidityUsd < minTvlUsdc) {
115
+ continue;
116
+ }
117
+ }
118
+
119
+ const netSpreadPct = grossSpreadPct - totalImpactPct;
100
120
  if (netSpreadPct < minSpreadPct) continue;
101
121
 
102
122
  rows.push({
@@ -109,8 +129,14 @@ function buildCrossVenueArbOpportunities(payload, options) {
109
129
  polymarketUrl: polyLeg.url || null,
110
130
  pandoraYesPct: toFiniteNumber(pandoraLeg.yesPct),
111
131
  polymarketYesPct: toFiniteNumber(polyLeg.yesPct),
132
+ pandoraLiquidityUsd,
133
+ polymarketLiquidityUsd,
134
+ minLegLiquidityUsd,
135
+ grossSpreadPct: roundNumber(grossSpreadPct, 6),
112
136
  spreadYesPct,
113
137
  spreadNoPct,
138
+ feeImpactPct,
139
+ slippageImpactPct,
114
140
  netSpreadPct: roundNumber(netSpreadPct, 6),
115
141
  confidenceScore: toFiniteNumber(item.confidenceScore),
116
142
  riskFlags: Array.isArray(item.riskFlags) ? item.riskFlags : [],
@@ -314,6 +340,10 @@ function parseArbScanFlags(args, deps) {
314
340
  amountUsdc: 100,
315
341
  combinatorial: false,
316
342
  maxBundleSize: 4,
343
+ similarityThreshold: 0.35,
344
+ minTokenScore: 0.12,
345
+ maxCloseDiffHours: 24,
346
+ questionContains: null,
317
347
  intervalMs: 5_000,
318
348
  iterations: null,
319
349
  };
@@ -380,6 +410,29 @@ function parseArbScanFlags(args, deps) {
380
410
  i += 1;
381
411
  continue;
382
412
  }
413
+ if (token === '--similarity-threshold') {
414
+ options.similarityThreshold = parseNumber(requireFlagValue(rest, i, '--similarity-threshold'), '--similarity-threshold');
415
+ i += 1;
416
+ continue;
417
+ }
418
+ if (token === '--min-token-score') {
419
+ options.minTokenScore = parseNumber(requireFlagValue(rest, i, '--min-token-score'), '--min-token-score');
420
+ i += 1;
421
+ continue;
422
+ }
423
+ if (token === '--max-close-diff-hours') {
424
+ options.maxCloseDiffHours = parsePositiveNumber(
425
+ requireFlagValue(rest, i, '--max-close-diff-hours'),
426
+ '--max-close-diff-hours',
427
+ );
428
+ i += 1;
429
+ continue;
430
+ }
431
+ if (token === '--question-contains') {
432
+ options.questionContains = String(requireFlagValue(rest, i, '--question-contains')).trim();
433
+ i += 1;
434
+ continue;
435
+ }
383
436
  if (token === '--interval-ms') {
384
437
  options.intervalMs = parsePositiveInteger(requireFlagValue(rest, i, '--interval-ms'), '--interval-ms');
385
438
  i += 1;
@@ -417,6 +470,12 @@ function parseArbScanFlags(args, deps) {
417
470
  if (options.slippagePctPerLeg < 0) {
418
471
  throw new CliError('INVALID_FLAG_VALUE', '--slippage-pct-per-leg must be >= 0.');
419
472
  }
473
+ if (options.similarityThreshold < 0 || options.similarityThreshold > 1) {
474
+ throw new CliError('INVALID_FLAG_VALUE', '--similarity-threshold must be between 0 and 1.');
475
+ }
476
+ if (options.minTokenScore < 0 || options.minTokenScore > 1) {
477
+ throw new CliError('INVALID_FLAG_VALUE', '--min-token-score must be between 0 and 1.');
478
+ }
420
479
 
421
480
  if (!Number.isInteger(options.maxBundleSize) || options.maxBundleSize < 3) {
422
481
  throw new CliError('INVALID_FLAG_VALUE', '--max-bundle-size must be an integer >= 3.');
@@ -530,15 +589,16 @@ function createRunArbCommand(deps) {
530
589
  timeoutMs: shared.timeoutMs,
531
590
  chainId: null,
532
591
  venues: ['pandora', 'polymarket'],
533
- limit: options.limit,
592
+ limit: Math.max(options.limit * 4, 100),
534
593
  minSpreadPct: options.minNetSpreadPct,
535
594
  minLiquidityUsd: options.minTvlUsdc,
536
- maxCloseDiffHours: 24,
537
- similarityThreshold: 0.86,
595
+ maxCloseDiffHours: options.maxCloseDiffHours,
596
+ similarityThreshold: options.similarityThreshold,
597
+ minTokenScore: options.minTokenScore,
538
598
  crossVenueOnly: true,
539
599
  withRules: false,
540
600
  includeSimilarity: false,
541
- questionContains: null,
601
+ questionContains: options.questionContains,
542
602
  });
543
603
  pairwiseOpportunities = buildCrossVenueArbOpportunities(crossVenuePayload, options);
544
604
  combinatorialOpportunities = [];
@@ -626,6 +686,10 @@ function createRunArbCommand(deps) {
626
686
  amountUsdc: options.amountUsdc,
627
687
  combinatorial: options.combinatorial,
628
688
  maxBundleSize: options.maxBundleSize,
689
+ similarityThreshold: options.similarityThreshold,
690
+ minTokenScore: options.minTokenScore,
691
+ maxCloseDiffHours: options.maxCloseDiffHours,
692
+ questionContains: options.questionContains,
629
693
  },
630
694
  opportunities: iterationSnapshots.flatMap((row) => row.opportunities),
631
695
  snapshots: iterationSnapshots,
@@ -645,6 +709,7 @@ function createRunArbCommand(deps) {
645
709
  module.exports = {
646
710
  ARB_USAGE,
647
711
  buildArbOpportunities,
712
+ buildCrossVenueArbOpportunities,
648
713
  buildCombinatorialArbOpportunities,
649
714
  createRunArbCommand,
650
715
  parseArbScanFlags,
@@ -8,6 +8,7 @@ const {
8
8
  const { toNumber, round } = require('./shared/utils.cjs');
9
9
 
10
10
  const ARBITRAGE_SCHEMA_VERSION = '1.1.0';
11
+ const DEFAULT_MIN_TOKEN_SCORE = 0.12;
11
12
 
12
13
  function toUsdc(raw) {
13
14
  const numeric = toNumber(raw);
@@ -170,6 +171,9 @@ async function fetchPandoraLegs(options, diagnostics) {
170
171
  }
171
172
 
172
173
  function buildGroups(legs, options) {
174
+ const minTokenScore = Number.isFinite(options && options.minTokenScore)
175
+ ? Number(options.minTokenScore)
176
+ : DEFAULT_MIN_TOKEN_SCORE;
173
177
  const parent = new Map();
174
178
  const acceptedPairChecks = new Map();
175
179
  const makePairKey = (a, b) => [a, b].sort().join('|');
@@ -202,6 +206,7 @@ function buildGroups(legs, options) {
202
206
  if (options.crossVenueOnly && left.venue === right.venue) continue;
203
207
 
204
208
  const similarity = questionSimilarityBreakdown(left.question, right.question);
209
+ if (similarity.tokenScore < minTokenScore) continue;
205
210
  if (similarity.score < options.similarityThreshold) continue;
206
211
 
207
212
  let closeDiffHours = null;
@@ -587,6 +592,9 @@ async function scanArbitrage(options) {
587
592
  timeoutMs: options.timeoutMs,
588
593
  limit: Math.max(options.limit * 3, 100),
589
594
  });
595
+ if (Array.isArray(poly.diagnostics) && poly.diagnostics.length) {
596
+ diagnostics.push(...poly.diagnostics);
597
+ }
590
598
 
591
599
  const filtered = poly.items.filter((item) => {
592
600
  if (!item.question) return false;
@@ -627,6 +635,9 @@ async function scanArbitrage(options) {
627
635
  minLiquidityUsd: options.minLiquidityUsd,
628
636
  maxCloseDiffHours: options.maxCloseDiffHours,
629
637
  similarityThreshold: options.similarityThreshold,
638
+ minTokenScore: Number.isFinite(options.minTokenScore)
639
+ ? options.minTokenScore
640
+ : DEFAULT_MIN_TOKEN_SCORE,
630
641
  crossVenueOnly: options.crossVenueOnly,
631
642
  withRules: options.withRules,
632
643
  includeSimilarity: options.includeSimilarity,
@@ -694,6 +705,9 @@ async function scanArbitrage(options) {
694
705
  minLiquidityUsd: options.minLiquidityUsd,
695
706
  maxCloseDiffHours: options.maxCloseDiffHours,
696
707
  similarityThreshold: options.similarityThreshold,
708
+ minTokenScore: Number.isFinite(options.minTokenScore)
709
+ ? options.minTokenScore
710
+ : DEFAULT_MIN_TOKEN_SCORE,
697
711
  crossVenueOnly: options.crossVenueOnly,
698
712
  withRules: options.withRules,
699
713
  includeSimilarity: options.includeSimilarity,
@@ -1153,6 +1153,7 @@ function createCoreCommandFlagParsers(deps) {
1153
1153
  minLiquidityUsd: 1000,
1154
1154
  maxCloseDiffHours: 24,
1155
1155
  similarityThreshold: 0.86,
1156
+ minTokenScore: 0.12,
1156
1157
  crossVenueOnly: true,
1157
1158
  withRules: false,
1158
1159
  includeSimilarity: false,
@@ -1186,7 +1187,7 @@ function createCoreCommandFlagParsers(deps) {
1186
1187
  continue;
1187
1188
  }
1188
1189
  if (token === '--min-spread-pct') {
1189
- options.minSpreadPct = parsePositiveNumber(requireFlagValue(args, i, '--min-spread-pct'), '--min-spread-pct');
1190
+ options.minSpreadPct = parseNumber(requireFlagValue(args, i, '--min-spread-pct'), '--min-spread-pct');
1190
1191
  i += 1;
1191
1192
  continue;
1192
1193
  }
@@ -1214,6 +1215,14 @@ function createCoreCommandFlagParsers(deps) {
1214
1215
  i += 1;
1215
1216
  continue;
1216
1217
  }
1218
+ if (token === '--min-token-score') {
1219
+ options.minTokenScore = parseNumber(requireFlagValue(args, i, '--min-token-score'), '--min-token-score');
1220
+ if (options.minTokenScore < 0 || options.minTokenScore > 1) {
1221
+ throw new CliError('INVALID_FLAG_VALUE', '--min-token-score must be between 0 and 1.');
1222
+ }
1223
+ i += 1;
1224
+ continue;
1225
+ }
1217
1226
  if (token === '--cross-venue-only') {
1218
1227
  options.crossVenueOnly = true;
1219
1228
  continue;
@@ -1249,6 +1258,10 @@ function createCoreCommandFlagParsers(deps) {
1249
1258
  throw new CliError('UNKNOWN_FLAG', `Unknown flag for arbitrage: ${token}`);
1250
1259
  }
1251
1260
 
1261
+ if (options.minSpreadPct < 0) {
1262
+ throw new CliError('INVALID_FLAG_VALUE', '--min-spread-pct must be >= 0.');
1263
+ }
1264
+
1252
1265
  return options;
1253
1266
  }
1254
1267
 
@@ -2,6 +2,7 @@ const { ClobClient, Chain } = require('@polymarket/clob-client');
2
2
  const { toNumber } = require('./shared/utils.cjs');
3
3
 
4
4
  const DEFAULT_POLYMARKET_HOST = 'https://clob.polymarket.com';
5
+ const DEFAULT_POLYMARKET_GAMMA_HOST = 'https://gamma-api.polymarket.com';
5
6
 
6
7
  function toTimestampSeconds(value) {
7
8
  if (!value) return null;
@@ -53,6 +54,66 @@ function normalizeTokens(tokens) {
53
54
  };
54
55
  }
55
56
 
57
+ function safeParseJsonArray(value) {
58
+ if (Array.isArray(value)) return value;
59
+ if (typeof value !== 'string') return null;
60
+ try {
61
+ const parsed = JSON.parse(value);
62
+ return Array.isArray(parsed) ? parsed : null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function buildTokensFromGammaPayload(row) {
69
+ const outcomes = safeParseJsonArray(row && row.outcomes) || safeParseJsonArray(row && row.outcomeNames);
70
+ const prices = safeParseJsonArray(row && row.outcomePrices) || safeParseJsonArray(row && row.prices);
71
+ if (!Array.isArray(outcomes) || !Array.isArray(prices) || outcomes.length !== prices.length || !outcomes.length) {
72
+ return null;
73
+ }
74
+ const tokens = [];
75
+ for (let index = 0; index < outcomes.length; index += 1) {
76
+ tokens.push({
77
+ outcome: String(outcomes[index] || ''),
78
+ price: prices[index],
79
+ });
80
+ }
81
+ return tokens;
82
+ }
83
+
84
+ function mapGammaRow(row) {
85
+ const tokens =
86
+ (Array.isArray(row && row.tokens) ? row.tokens : null) ||
87
+ (Array.isArray(row && row.outcomePrices) && Array.isArray(row && row.outcomes)
88
+ ? row.outcomes.map((outcome, index) => ({ outcome, price: row.outcomePrices[index] }))
89
+ : null) ||
90
+ buildTokensFromGammaPayload(row);
91
+ const mapped = normalizeTokens(tokens || []);
92
+ const marketId = row && (row.conditionId || row.condition_id || row.id || row.questionID) ? String(
93
+ row.conditionId || row.condition_id || row.id || row.questionID,
94
+ ) : null;
95
+ const closeTimestamp = toTimestampSeconds(
96
+ row && (row.endDateIso || row.end_date_iso || row.endDate || row.game_start_time || row.closedTime),
97
+ );
98
+ return {
99
+ legId: `polymarket:${String(marketId || '')}`,
100
+ venue: 'polymarket',
101
+ marketId,
102
+ question: row && (row.question || row.title || row.description) ? String(row.question || row.title || row.description) : null,
103
+ closeTimestamp,
104
+ yesPct: mapped.yes,
105
+ noPct: mapped.no,
106
+ liquidityUsd: toNumber(row && (row.liquidityNum || row.liquidity || row.liquidityClob)),
107
+ volumeUsd: toNumber(row && (row.volumeNum || row.volume || row.volumeClob)),
108
+ url: row && (row.market_slug || row.slug) ? `https://polymarket.com/event/${String(row.market_slug || row.slug)}` : null,
109
+ oddsSource: 'polymarket:gamma-markets',
110
+ diagnostics: mapped.diagnostics,
111
+ rules: row && row.description ? String(row.description) : null,
112
+ sources: [],
113
+ pollStatus: null,
114
+ };
115
+ }
116
+
56
117
  function mapPolymarketRow(row) {
57
118
  const mapped = normalizeTokens(row.tokens || []);
58
119
  const question = row.question || row.description || null;
@@ -101,41 +162,138 @@ async function fetchMockPolymarketMarkets(mockUrl, timeoutMs) {
101
162
  }
102
163
  }
103
164
 
104
- async function fetchPolymarketMarkets(options = {}) {
105
- const host = options.host || DEFAULT_POLYMARKET_HOST;
165
+ async function fetchJsonWithTimeout(url, timeoutMs) {
166
+ const controller = new AbortController();
167
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
168
+ try {
169
+ const response = await fetch(url, {
170
+ method: 'GET',
171
+ headers: { accept: 'application/json' },
172
+ signal: controller.signal,
173
+ });
174
+ if (!response.ok) {
175
+ throw new Error(`HTTP ${response.status} from ${url}`);
176
+ }
177
+ return response.json();
178
+ } finally {
179
+ clearTimeout(timeout);
180
+ }
181
+ }
182
+
183
+ function normalizeGammaMarketRows(payload) {
184
+ if (Array.isArray(payload)) return payload;
185
+ if (payload && Array.isArray(payload.data)) return payload.data;
186
+ if (payload && Array.isArray(payload.markets)) return payload.markets;
187
+ return [];
188
+ }
189
+
190
+ async function fetchGammaPolymarketMarkets(options = {}) {
191
+ const gammaHostRaw =
192
+ options.gammaHost ||
193
+ process.env.POLYMARKET_GAMMA_HOST ||
194
+ DEFAULT_POLYMARKET_GAMMA_HOST;
195
+ const gammaHost = String(gammaHostRaw || '').replace(/\/+$/, '');
106
196
  const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
107
197
  const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
108
198
 
199
+ const rows = [];
200
+ let offset = 0;
201
+ let loopCount = 0;
202
+ while (rows.length < limit && loopCount < 10) {
203
+ loopCount += 1;
204
+ const pageLimit = Math.min(200, Math.max(25, limit - rows.length));
205
+ const url =
206
+ `${gammaHost}/markets?active=true&closed=false&archived=false&order=volume` +
207
+ `&ascending=false&limit=${pageLimit}&offset=${offset}`;
208
+ const payload = await fetchJsonWithTimeout(url, timeoutMs);
209
+ const batch = normalizeGammaMarketRows(payload);
210
+ if (!batch.length) break;
211
+ rows.push(...batch);
212
+ if (batch.length < pageLimit) break;
213
+ offset += batch.length;
214
+ }
215
+
216
+ return {
217
+ host: gammaHost,
218
+ rows,
219
+ };
220
+ }
221
+
222
+ async function fetchClobPolymarketMarkets(options = {}) {
223
+ const host = options.host || DEFAULT_POLYMARKET_HOST;
224
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
225
+ const client = new ClobClient(host, Chain.POLYGON);
226
+ const rows = [];
227
+ let cursor;
228
+ let loops = 0;
229
+ while (rows.length < limit && loops < 8) {
230
+ loops += 1;
231
+ const page = cursor ? await client.getMarkets(cursor) : await client.getMarkets();
232
+ const data = Array.isArray(page && page.data) ? page.data : [];
233
+ rows.push(...data);
234
+ if (!page || !page.next_cursor || page.next_cursor === cursor) break;
235
+ cursor = page.next_cursor;
236
+ }
237
+ return { host, rows };
238
+ }
239
+
240
+ async function fetchPolymarketMarkets(options = {}) {
241
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 100;
242
+ const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
243
+ const diagnostics = [];
109
244
  let rows = [];
245
+ let source = 'polymarket:clob';
246
+ let host = options.host || DEFAULT_POLYMARKET_HOST;
110
247
 
111
248
  if (options.mockUrl) {
112
249
  rows = await fetchMockPolymarketMarkets(options.mockUrl, timeoutMs);
250
+ source = 'polymarket:mock';
113
251
  } else {
114
- const client = new ClobClient(host, Chain.POLYGON);
115
- let cursor;
116
- let loops = 0;
117
- while (rows.length < limit && loops < 5) {
118
- loops += 1;
119
- const page = cursor ? await client.getMarkets(cursor) : await client.getMarkets();
120
- const data = Array.isArray(page && page.data) ? page.data : [];
121
- rows.push(...data);
122
- if (!page || !page.next_cursor || page.next_cursor === cursor) {
123
- break;
252
+ let gammaResult = null;
253
+ try {
254
+ const preferredGammaHost =
255
+ options.host && /gamma-api\.polymarket\.com/i.test(String(options.host))
256
+ ? options.host
257
+ : options.gammaHost;
258
+ gammaResult = await fetchGammaPolymarketMarkets({
259
+ gammaHost: preferredGammaHost,
260
+ timeoutMs,
261
+ limit,
262
+ });
263
+ if (Array.isArray(gammaResult.rows) && gammaResult.rows.length) {
264
+ rows = gammaResult.rows;
265
+ source = 'polymarket:gamma';
266
+ host = gammaResult.host;
124
267
  }
125
- cursor = page.next_cursor;
268
+ } catch (err) {
269
+ diagnostics.push(`Gamma markets fetch failed: ${err && err.message ? err.message : String(err)}`);
270
+ }
271
+
272
+ if (!rows.length) {
273
+ const clob = await fetchClobPolymarketMarkets({
274
+ host: options.host,
275
+ timeoutMs,
276
+ limit,
277
+ });
278
+ rows = clob.rows;
279
+ source = 'polymarket:clob';
280
+ host = clob.host;
126
281
  }
127
282
  }
128
283
 
129
- const mapped = rows.slice(0, limit).map(mapPolymarketRow);
284
+ const mapper = source === 'polymarket:gamma' ? mapGammaRow : mapPolymarketRow;
285
+ const mapped = rows.slice(0, limit).map(mapper);
130
286
  return {
131
287
  host,
132
- source: options.mockUrl ? 'polymarket:mock' : 'polymarket:clob',
288
+ source,
133
289
  count: mapped.length,
134
290
  items: mapped,
291
+ diagnostics,
135
292
  };
136
293
  }
137
294
 
138
295
  module.exports = {
139
296
  DEFAULT_POLYMARKET_HOST,
297
+ DEFAULT_POLYMARKET_GAMMA_HOST,
140
298
  fetchPolymarketMarkets,
141
299
  };
@@ -58,8 +58,10 @@ function createRunScanCommand(deps) {
58
58
  const indexerUrl = resolveIndexerUrl(shared.indexerUrl);
59
59
 
60
60
  const options = parseMarketsListFlags(shared.rest);
61
+ options.expand = true;
61
62
  options.withOdds = true;
62
63
 
64
+ let hedgeableDiagnostics = [];
63
65
  let { items, pageInfo, unfilteredCount } = await fetchMarketsListPage(indexerUrl, options, shared.timeoutMs);
64
66
  if (options.hedgeable && typeof filterHedgeableMarkets === 'function') {
65
67
  const filtered = await filterHedgeableMarkets({
@@ -72,6 +74,9 @@ function createRunScanCommand(deps) {
72
74
  if (typeof filtered.unfilteredCount === 'number') {
73
75
  unfilteredCount = filtered.unfilteredCount;
74
76
  }
77
+ if (Array.isArray(filtered && filtered.diagnostics)) {
78
+ hedgeableDiagnostics = filtered.diagnostics;
79
+ }
75
80
  }
76
81
  const enrichmentContext = await buildMarketsEnrichmentContext(indexerUrl, items, options, shared.timeoutMs);
77
82
  const payload = buildMarketsListPayload(indexerUrl, options, items, pageInfo, {
@@ -79,6 +84,7 @@ function createRunScanCommand(deps) {
79
84
  scanMode: true,
80
85
  enrichmentContext,
81
86
  unfilteredCount,
87
+ externalDiagnostics: hedgeableDiagnostics,
82
88
  });
83
89
 
84
90
  emitSuccess(context.outputMode, 'scan', payload, renderScanTable);