pandora-cli-skills 1.1.55 → 1.1.56

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.56
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -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 : [],
@@ -530,11 +556,12 @@ function createRunArbCommand(deps) {
530
556
  timeoutMs: shared.timeoutMs,
531
557
  chainId: null,
532
558
  venues: ['pandora', 'polymarket'],
533
- limit: options.limit,
559
+ limit: Math.max(options.limit * 4, 100),
534
560
  minSpreadPct: options.minNetSpreadPct,
535
561
  minLiquidityUsd: options.minTvlUsdc,
536
562
  maxCloseDiffHours: 24,
537
563
  similarityThreshold: 0.86,
564
+ minTokenScore: 0.12,
538
565
  crossVenueOnly: true,
539
566
  withRules: false,
540
567
  includeSimilarity: false,
@@ -645,6 +672,7 @@ function createRunArbCommand(deps) {
645
672
  module.exports = {
646
673
  ARB_USAGE,
647
674
  buildArbOpportunities,
675
+ buildCrossVenueArbOpportunities,
648
676
  buildCombinatorialArbOpportunities,
649
677
  createRunArbCommand,
650
678
  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);