pandora-cli-skills 1.1.47 → 1.1.48

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/README.md CHANGED
@@ -122,6 +122,7 @@ pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
122
122
  - `pandora odds record|history`
123
123
  - `pandora autopilot run|once`
124
124
  - `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close`
125
+ - `mirror browse` supports `--polymarket-tag-id|--polymarket-tag-ids` (aliases `--sport-tag-id|--sport-tag-ids`) for sports-tagged Gamma event discovery.
125
126
  - `pandora polymarket check|approve|preflight|trade`
126
127
  - `pandora resolve`
127
128
  - `pandora lp add|remove|positions`
@@ -200,6 +200,7 @@ Mirror advanced flags (for operator tuning):
200
200
  - `--sync-interval-ms <ms>` on `mirror go` to control auto-sync tick cadence.
201
201
  - `--oracle <address>` / `--factory <address>` on `mirror deploy` and `mirror go` for explicit contract overrides.
202
202
  - `--polymarket-gamma-mock-url <url>` on `mirror browse|plan|verify|go|sync|status` for deterministic mock-source testing.
203
+ - `--polymarket-tag-id <id>` / `--polymarket-tag-ids <csv>` on `mirror browse` (aliases: `--sport-tag-id`, `--sport-tag-ids`) to query sports-tagged Gamma events.
203
204
  - `--no-stream` on `mirror sync` to disable per-tick stdout line streaming in run mode.
204
205
  - `--pid-file <path>` on `mirror sync stop|status` for explicit daemon process selection.
205
206
 
@@ -219,7 +220,7 @@ Mirror advanced flags (for operator tuning):
219
220
  - `pandora export --wallet <0x...> --format csv --out ./trades.csv`
220
221
  - `pandora arbitrage --venues pandora,polymarket --min-spread-pct 2 --cross-venue-only --with-rules --include-similarity`
221
222
  - `pandora autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper`
222
- - `pandora mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
223
+ - `pandora mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
223
224
  - `pandora mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity`
224
225
  - `pandora mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper`
225
226
  - `pandora mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity`
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.46
4
+ version: 1.1.48
5
5
  ---
6
6
 
7
7
  # Pandora CLI & Skills
@@ -317,7 +317,7 @@ pandora --output json history --wallet <0x...> --limit 50
317
317
  pandora --output json export --wallet <0x...> --format csv --out ./trades.csv
318
318
  pandora --output json arbitrage --venues pandora,polymarket --min-spread-pct 3 --cross-venue-only --with-rules --include-similarity
319
319
  pandora --output json autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper
320
- pandora --output json mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
320
+ pandora --output json mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
321
321
  pandora --output json mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity
322
322
  pandora --output json mirror lp-explain --liquidity-usdc 10000 --source-yes-pct 58
323
323
  pandora --output json mirror hedge-calc --reserve-yes-usdc 8 --reserve-no-usdc 12 --excess-no-usdc 2 --polymarket-yes-pct 60
@@ -562,7 +562,8 @@ Error envelope:
562
562
  - `{ ok: true, command: "autopilot", data: { schemaVersion, generatedAt, strategyHash, mode, executeLive, stateFile, killSwitchFile, iterationsRequested, iterationsCompleted, stoppedReason?, parameters: { marketAddress, side, amountUsdc, triggerYesBelow?, triggerYesAbove?, intervalMs, cooldownMs, maxAmountUsdc?, maxOpenExposureUsdc, dailySpendCapUsdc, maxTradesPerDay }, state, actionCount, actions[], snapshots[], webhookReports[] } }`
563
563
  - `mirror browse`:
564
564
  - `{ ok: true, command: "mirror.browse", data: { schemaVersion, generatedAt, source, gammaApiError, filters, count, items[], diagnostics[] } }`
565
- - each candidate row can include `existingMirror: { marketAddress, similarity } | null`.
565
+ - `filters` can include `polymarketTagIds[]` when using `--polymarket-tag-id|--polymarket-tag-ids` (or `--sport-tag-id|--sport-tag-ids` aliases).
566
+ - each candidate row can include `eventId`, `eventSlug`, `eventTitle`, and `existingMirror: { marketAddress, similarity } | null`.
566
567
  - `mirror go`:
567
568
  - `{ ok: true, command: "mirror.go", data: { schemaVersion, generatedAt, mode, plan, deploy, verify, sync, polymarketPreflight, suggestedSyncCommand, trustManifest, diagnostics[] } }`
568
569
  - `plan` is the same payload shape as `mirror.plan`; `deploy` is the same payload shape as `mirror.deploy`; `sync` is null unless `--auto-sync` is used.
@@ -91,7 +91,7 @@ function createRunMirrorCommand(deps) {
91
91
  console.log('');
92
92
  console.log('Subcommands:');
93
93
  console.log(
94
- ' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
94
+ ' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
95
95
  );
96
96
  console.log(
97
97
  ' plan --source polymarket --polymarket-market-id <id>|--polymarket-slug <slug> [--chain-id <id>] [--target-slippage-bps <n>] [--turnover-target <n>] [--depth-slippage-bps <n>] [--safety-multiplier <n>] [--min-liquidity-usdc <n>] [--max-liquidity-usdc <n>] [--with-rules] [--include-similarity] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
@@ -22,12 +22,12 @@ module.exports = async function handleMirrorBrowse({ shared, context, deps }) {
22
22
  context.outputMode,
23
23
  'mirror.browse.help',
24
24
  commandHelpPayload(
25
- 'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
25
+ 'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
26
26
  ),
27
27
  );
28
28
  } else {
29
29
  console.log(
30
- 'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
30
+ 'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
31
31
  );
32
32
  }
33
33
  return;
@@ -395,6 +395,7 @@ async function browseMirrorMarkets(options = {}) {
395
395
  gammaUrl: options.polymarketGammaUrl,
396
396
  gammaMockUrl: options.polymarketGammaMockUrl,
397
397
  mockUrl: options.polymarketMockUrl,
398
+ polymarketTagIds: Array.isArray(options.polymarketTagIds) ? options.polymarketTagIds : [],
398
399
  timeoutMs: options.timeoutMs,
399
400
  minYesPct: options.minYesPct,
400
401
  maxYesPct: options.maxYesPct,
@@ -39,8 +39,13 @@ function createParseMirrorBrowseFlags(deps) {
39
39
  polymarketGammaUrl: null,
40
40
  polymarketGammaMockUrl: null,
41
41
  polymarketMockUrl: null,
42
+ polymarketTagIds: [],
42
43
  };
43
44
 
45
+ function pushTagId(rawValue, flagName) {
46
+ options.polymarketTagIds.push(parsePositiveInteger(rawValue, flagName));
47
+ }
48
+
44
49
  for (let i = 0; i < args.length; i += 1) {
45
50
  const token = args[i];
46
51
  if (token === '--min-yes-pct') {
@@ -98,6 +103,26 @@ function createParseMirrorBrowseFlags(deps) {
98
103
  i += 1;
99
104
  continue;
100
105
  }
106
+ if (token === '--polymarket-tag-id' || token === '--sport-tag-id') {
107
+ pushTagId(requireFlagValue(args, i, token), token);
108
+ i += 1;
109
+ continue;
110
+ }
111
+ if (token === '--polymarket-tag-ids' || token === '--sport-tag-ids') {
112
+ const raw = requireFlagValue(args, i, token);
113
+ const values = String(raw)
114
+ .split(',')
115
+ .map((value) => value.trim())
116
+ .filter(Boolean);
117
+ if (!values.length) {
118
+ throw new CliError('INVALID_FLAG_VALUE', `${token} must include at least one positive integer tag id.`);
119
+ }
120
+ for (const value of values) {
121
+ pushTagId(value, token);
122
+ }
123
+ i += 1;
124
+ continue;
125
+ }
101
126
  throw new CliError('UNKNOWN_FLAG', `Unknown flag for mirror browse: ${token}`);
102
127
  }
103
128
 
@@ -105,6 +130,10 @@ function createParseMirrorBrowseFlags(deps) {
105
130
  throw new CliError('INVALID_ARGS', '--min-yes-pct cannot be greater than --max-yes-pct.');
106
131
  }
107
132
 
133
+ if (options.polymarketTagIds.length) {
134
+ options.polymarketTagIds = Array.from(new Set(options.polymarketTagIds));
135
+ }
136
+
108
137
  return options;
109
138
  };
110
139
  }
@@ -405,6 +405,9 @@ function normalizeMarketRow(row) {
405
405
  ).trim() || null,
406
406
  slug: String((row && (row.market_slug || row.marketSlug || row.slug)) || '').trim() || null,
407
407
  question: extractQuestionText(row),
408
+ eventId: toStringOrNull(row && (row.event_id || row.eventId)),
409
+ eventSlug: toStringOrNull(row && (row.event_slug || row.eventSlug)),
410
+ eventTitle: toStringOrNull(row && (row.event_title || row.eventTitle)),
408
411
  description: rulesSections.length ? rulesSections.join('\n\n') : null,
409
412
  closeTimestamp: toTimestampSeconds(
410
413
  row &&
@@ -479,6 +482,14 @@ function parseMarketsPayload(payload) {
479
482
  return [];
480
483
  }
481
484
 
485
+ function parseEventsPayload(payload) {
486
+ if (Array.isArray(payload)) return payload;
487
+ if (payload && Array.isArray(payload.events)) return payload.events;
488
+ if (payload && payload.data && Array.isArray(payload.data.events)) return payload.data.events;
489
+ if (payload && Array.isArray(payload.data)) return payload.data;
490
+ return [];
491
+ }
492
+
482
493
  async function fetchJson(url, timeoutMs) {
483
494
  const controller = new AbortController();
484
495
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -530,6 +541,15 @@ function buildGammaUrl(baseUrl, params) {
530
541
  return url.toString();
531
542
  }
532
543
 
544
+ function buildGammaEventsUrl(baseUrl, params) {
545
+ const url = new URL(`${baseUrl}/events`);
546
+ for (const [key, value] of Object.entries(params || {})) {
547
+ if (value === null || value === undefined || value === '') continue;
548
+ url.searchParams.set(key, String(value));
549
+ }
550
+ return url.toString();
551
+ }
552
+
533
553
  async function fetchGammaRows(params, options = {}, diagnostics = []) {
534
554
  const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
535
555
  const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
@@ -543,6 +563,120 @@ async function fetchGammaRows(params, options = {}, diagnostics = []) {
543
563
  }
544
564
  }
545
565
 
566
+ function makeMarketDedupeKey(row) {
567
+ return (
568
+ normalizeText(
569
+ row &&
570
+ (row.condition_id ||
571
+ row.conditionId ||
572
+ row.question_id ||
573
+ row.questionId ||
574
+ row.market_id ||
575
+ row.marketId ||
576
+ row.id ||
577
+ row.slug),
578
+ ) || null
579
+ );
580
+ }
581
+
582
+ function flattenEventMarkets(events) {
583
+ const output = [];
584
+ const seen = new Set();
585
+
586
+ for (const event of Array.isArray(events) ? events : []) {
587
+ const markets = Array.isArray(event && event.markets) ? event.markets : [];
588
+ for (const market of markets) {
589
+ if (!market || typeof market !== 'object') continue;
590
+ const dedupeKey = makeMarketDedupeKey(market);
591
+ if (dedupeKey && seen.has(dedupeKey)) continue;
592
+ if (dedupeKey) seen.add(dedupeKey);
593
+ output.push({
594
+ ...market,
595
+ event_id: event && event.id !== undefined ? event.id : null,
596
+ event_slug: event && event.slug ? event.slug : null,
597
+ event_title: event && event.title ? event.title : null,
598
+ event_tags: Array.isArray(event && event.tags) ? event.tags : [],
599
+ });
600
+ }
601
+ }
602
+
603
+ return output;
604
+ }
605
+
606
+ function normalizeTagIdList(input) {
607
+ const values = Array.isArray(input) ? input : [];
608
+ const normalized = [];
609
+ for (const value of values) {
610
+ const numeric = Number(value);
611
+ if (!Number.isFinite(numeric)) continue;
612
+ const asInt = Math.trunc(numeric);
613
+ if (asInt <= 0) continue;
614
+ normalized.push(asInt);
615
+ }
616
+ return Array.from(new Set(normalized));
617
+ }
618
+
619
+ async function fetchGammaRowsByTagIds(params, options = {}, diagnostics = []) {
620
+ const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
621
+ const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
622
+ const tagIds = normalizeTagIdList(params && params.tagIds);
623
+ if (!tagIds.length) return [];
624
+
625
+ if (options.gammaMockUrl) {
626
+ try {
627
+ const payload = await fetchJson(options.gammaMockUrl, timeoutMs);
628
+ const events = parseEventsPayload(payload);
629
+ return flattenEventMarkets(events);
630
+ } catch (err) {
631
+ diagnostics.push(`Gamma sports-events request failed (${options.gammaMockUrl}): ${formatNetworkError(err)}`);
632
+ return [];
633
+ }
634
+ }
635
+
636
+ const allRows = [];
637
+ const perTagResults = await Promise.all(
638
+ tagIds.map(async (tagId) => {
639
+ const queryParams = {
640
+ ...(params || {}),
641
+ tag_id: tagId,
642
+ };
643
+ delete queryParams.tagIds;
644
+ const targetUrl = buildGammaEventsUrl(gammaUrl, queryParams);
645
+ try {
646
+ const payload = await fetchJson(targetUrl, timeoutMs);
647
+ const events = parseEventsPayload(payload);
648
+ return {
649
+ rows: flattenEventMarkets(events),
650
+ error: null,
651
+ };
652
+ } catch (err) {
653
+ return {
654
+ rows: [],
655
+ error: `Gamma sports-events request failed (${targetUrl}): ${formatNetworkError(err)}`,
656
+ };
657
+ }
658
+ }),
659
+ );
660
+
661
+ for (const result of perTagResults) {
662
+ if (result.error) {
663
+ diagnostics.push(result.error);
664
+ continue;
665
+ }
666
+ allRows.push(...result.rows);
667
+ }
668
+
669
+ const deduped = [];
670
+ const seen = new Set();
671
+ for (const row of allRows) {
672
+ const dedupeKey = makeMarketDedupeKey(row);
673
+ if (dedupeKey && seen.has(dedupeKey)) continue;
674
+ if (dedupeKey) seen.add(dedupeKey);
675
+ deduped.push(row);
676
+ }
677
+ return deduped;
678
+ }
679
+
546
680
  function extractConditionId(row) {
547
681
  const value = toStringOrNull(
548
682
  row &&
@@ -998,21 +1132,38 @@ async function browsePolymarketMarkets(options = {}) {
998
1132
  const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
999
1133
  const requestedLimit = Number.isInteger(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 10;
1000
1134
  const scanLimit = Math.max(requestedLimit * 5, 100);
1135
+ const polymarketTagIds = normalizeTagIdList(options.polymarketTagIds);
1136
+ const useSportsEventsEndpoint = polymarketTagIds.length > 0;
1001
1137
 
1002
1138
  let rows = [];
1139
+ let sourceType = options.mockUrl ? 'polymarket:mock' : 'polymarket:gamma';
1003
1140
  if (options.mockUrl) {
1004
1141
  const payload = await fetchMockPayload(options.mockUrl, timeoutMs);
1005
1142
  rows = parseMarketsPayload(payload);
1006
1143
  } else {
1007
- rows = await fetchGammaRows(
1008
- {
1009
- active: true,
1010
- closed: false,
1011
- limit: Math.min(scanLimit, 500),
1012
- },
1013
- options,
1014
- diagnostics,
1015
- );
1144
+ if (useSportsEventsEndpoint) {
1145
+ rows = await fetchGammaRowsByTagIds(
1146
+ {
1147
+ tagIds: polymarketTagIds,
1148
+ active: true,
1149
+ closed: false,
1150
+ limit: Math.min(scanLimit, 500),
1151
+ },
1152
+ options,
1153
+ diagnostics,
1154
+ );
1155
+ sourceType = 'polymarket:gamma-events';
1156
+ } else {
1157
+ rows = await fetchGammaRows(
1158
+ {
1159
+ active: true,
1160
+ closed: false,
1161
+ limit: Math.min(scanLimit, 500),
1162
+ },
1163
+ options,
1164
+ diagnostics,
1165
+ );
1166
+ }
1016
1167
  }
1017
1168
 
1018
1169
  const minYesPct = toOptionalNumber(options.minYesPct);
@@ -1039,6 +1190,9 @@ async function browsePolymarketMarkets(options = {}) {
1039
1190
  const items = filtered.slice(0, requestedLimit).map((item) => ({
1040
1191
  marketId: item.marketId,
1041
1192
  slug: item.slug,
1193
+ eventId: item.eventId,
1194
+ eventSlug: item.eventSlug,
1195
+ eventTitle: item.eventTitle,
1042
1196
  question: item.question,
1043
1197
  closeTimestamp: item.closeTimestamp,
1044
1198
  yesPct: item.yesPct,
@@ -1048,16 +1202,16 @@ async function browsePolymarketMarkets(options = {}) {
1048
1202
  active: item.active,
1049
1203
  resolved: item.resolved,
1050
1204
  url: item.url,
1051
- sourceType: item.source || (options.mockUrl ? 'polymarket:mock' : 'polymarket:gamma'),
1205
+ sourceType: item.source || sourceType,
1052
1206
  }));
1053
1207
 
1054
1208
  const gammaApiError =
1055
- diagnostics.find((line) => /^Gamma request failed/i.test(String(line || ''))) || null;
1209
+ diagnostics.find((line) => /^Gamma( sports-events)? request failed/i.test(String(line || ''))) || null;
1056
1210
 
1057
1211
  return {
1058
1212
  schemaVersion: '1.0.0',
1059
1213
  generatedAt: new Date().toISOString(),
1060
- source: options.mockUrl ? 'polymarket:mock' : 'polymarket:gamma',
1214
+ source: sourceType,
1061
1215
  filters: {
1062
1216
  minYesPct,
1063
1217
  maxYesPct,
@@ -1066,6 +1220,7 @@ async function browsePolymarketMarkets(options = {}) {
1066
1220
  closesBefore,
1067
1221
  questionContains: options.questionContains || null,
1068
1222
  limit: requestedLimit,
1223
+ polymarketTagIds,
1069
1224
  },
1070
1225
  count: items.length,
1071
1226
  items,
@@ -264,6 +264,13 @@ function buildCommandDescriptors() {
264
264
  emits: ['sports.resolve.plan', 'sports.help'],
265
265
  dataSchema: '#/definitions/SportsResolvePlanPayload',
266
266
  }),
267
+ 'mirror.browse': commandDescriptor({
268
+ summary: 'Browse Polymarket mirror candidates with optional sports tag filters.',
269
+ usage:
270
+ 'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
271
+ emits: ['mirror.browse', 'mirror.browse.help'],
272
+ dataSchema: '#/definitions/MirrorBrowsePayload',
273
+ }),
267
274
  'mirror.plan': commandDescriptor({
268
275
  summary: 'Generate mirror sizing/distribution plan from Polymarket source.',
269
276
  usage:
@@ -812,6 +819,19 @@ function buildSchemaPayload() {
812
819
  generatedAt: { type: 'string', format: 'date-time' },
813
820
  },
814
821
  },
822
+ MirrorBrowsePayload: {
823
+ type: 'object',
824
+ properties: {
825
+ source: { type: 'string' },
826
+ gammaApiError: { type: ['string', 'null'] },
827
+ filters: { type: 'object' },
828
+ count: { type: 'integer' },
829
+ items: { type: 'array', items: { type: 'object' } },
830
+ diagnostics: { type: 'array', items: { type: 'string' } },
831
+ schemaVersion: { type: 'string' },
832
+ generatedAt: { type: 'string', format: 'date-time' },
833
+ },
834
+ },
815
835
  MirrorPlanPayload: {
816
836
  type: 'object',
817
837
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pandora-cli-skills",
3
- "version": "1.1.47",
3
+ "version": "1.1.48",
4
4
  "description": "Pandora CLI & Skills",
5
5
  "main": "cli/pandora.cjs",
6
6
  "bin": {
@@ -1090,6 +1090,9 @@ test('schema command returns envelope schema plus command descriptors', () => {
1090
1090
  assert.ok(payload.data.commandDescriptors.quote.emits.includes('quote'));
1091
1091
  assert.ok(payload.data.commandDescriptors.trade);
1092
1092
  assert.equal(payload.data.commandDescriptors.trade.dataSchema, '#/definitions/TradePayload');
1093
+ assert.ok(payload.data.commandDescriptors['mirror.browse']);
1094
+ assert.equal(payload.data.commandDescriptors['mirror.browse'].dataSchema, '#/definitions/MirrorBrowsePayload');
1095
+ assert.match(payload.data.commandDescriptors['mirror.browse'].usage, /--polymarket-tag-id/);
1093
1096
  assert.ok(payload.data.commandDescriptors['mirror.plan']);
1094
1097
  assert.equal(payload.data.commandDescriptors['mirror.plan'].dataSchema, '#/definitions/MirrorPlanPayload');
1095
1098
  assert.ok(payload.data.commandDescriptors['risk.show']);
@@ -1150,6 +1153,7 @@ test('schema command returns envelope schema plus command descriptors', () => {
1150
1153
  assert.ok(payload.data.definitions.ModelCorrelationPayload);
1151
1154
  assert.ok(payload.data.definitions.ModelDiagnosePayload);
1152
1155
  assert.ok(payload.data.definitions.ErrorRecoveryPayload);
1156
+ assert.ok(payload.data.definitions.MirrorBrowsePayload);
1153
1157
  });
1154
1158
 
1155
1159
  test('schema command rejects unknown trailing flags', () => {
@@ -4825,6 +4829,24 @@ test('mirror browse rejects invalid calendar rollover dates', () => {
4825
4829
  assert.match(payload.error.message, /real calendar date/);
4826
4830
  });
4827
4831
 
4832
+ test('mirror browse rejects invalid tag id values', () => {
4833
+ const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-id', '0']);
4834
+ assert.equal(result.status, 1);
4835
+ const payload = parseJsonOutput(result);
4836
+ assert.equal(payload.ok, false);
4837
+ assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
4838
+ assert.match(payload.error.message, /--polymarket-tag-id must be a positive integer/i);
4839
+ });
4840
+
4841
+ test('mirror browse rejects empty tag-id csv values', () => {
4842
+ const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-ids', ', ,']);
4843
+ assert.equal(result.status, 1);
4844
+ const payload = parseJsonOutput(result);
4845
+ assert.equal(payload.ok, false);
4846
+ assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
4847
+ assert.match(payload.error.message, /must include at least one positive integer tag id/i);
4848
+ });
4849
+
4828
4850
  test('boolean flags with --key=false do not silently flip behavior', () => {
4829
4851
  const result = runCli(['--output', 'json', 'scan', '--active=false']);
4830
4852
  assert.equal(result.status, 1);
@@ -4924,6 +4946,83 @@ test('mirror browse returns candidate markets with existing mirror hint', async
4924
4946
  }
4925
4947
  });
4926
4948
 
4949
+ test('mirror browse supports sports tag filters via gamma events endpoint', async () => {
4950
+ const gamma = await startJsonHttpServer((request) => {
4951
+ const parsed = new URL(request.url || '/', 'http://127.0.0.1');
4952
+ if (parsed.pathname !== '/events') {
4953
+ return { status: 404, body: { error: 'not found' } };
4954
+ }
4955
+
4956
+ const tagId = parsed.searchParams.get('tag_id');
4957
+ if (tagId !== '82') {
4958
+ return { body: { events: [] } };
4959
+ }
4960
+
4961
+ return {
4962
+ body: {
4963
+ events: [
4964
+ {
4965
+ id: 'evt-epl-1',
4966
+ slug: 'everton-v-burnley',
4967
+ title: 'Everton vs Burnley',
4968
+ markets: [
4969
+ {
4970
+ condition_id: 'poly-epl-c1',
4971
+ market_slug: 'everton-v-burnley-home',
4972
+ question: 'Will Everton beat Burnley?',
4973
+ end_date_iso: FIXED_MIRROR_CLOSE_ISO,
4974
+ active: true,
4975
+ closed: false,
4976
+ volume24hr: 550000,
4977
+ tokens: [
4978
+ { outcome: 'Yes', price: '0.605', token_id: 'poly-epl-yes-1' },
4979
+ { outcome: 'No', price: '0.395', token_id: 'poly-epl-no-1' },
4980
+ ],
4981
+ },
4982
+ ],
4983
+ },
4984
+ ],
4985
+ },
4986
+ };
4987
+ });
4988
+
4989
+ try {
4990
+ const result = await runCliAsync([
4991
+ '--output',
4992
+ 'json',
4993
+ 'mirror',
4994
+ 'browse',
4995
+ '--skip-dotenv',
4996
+ '--polymarket-gamma-url',
4997
+ gamma.url,
4998
+ '--polymarket-tag-id',
4999
+ '82',
5000
+ '--limit',
5001
+ '5',
5002
+ ]);
5003
+
5004
+ assert.equal(result.status, 0);
5005
+ const payload = parseJsonOutput(result);
5006
+ assert.equal(payload.ok, true);
5007
+ assert.equal(payload.command, 'mirror.browse');
5008
+ assert.equal(payload.data.source, 'polymarket:gamma-events');
5009
+ assert.deepEqual(payload.data.filters.polymarketTagIds, [82]);
5010
+ assert.equal(payload.data.count, 1);
5011
+ assert.equal(payload.data.items[0].eventSlug, 'everton-v-burnley');
5012
+ assert.equal(payload.data.items[0].eventTitle, 'Everton vs Burnley');
5013
+ assert.equal(payload.data.items[0].eventId, 'evt-epl-1');
5014
+
5015
+ const eventRequest = gamma.requests.find((entry) => String(entry.url || '').startsWith('/events?'));
5016
+ assert.ok(eventRequest);
5017
+ const parsed = new URL(eventRequest.url, 'http://127.0.0.1');
5018
+ assert.equal(parsed.searchParams.get('tag_id'), '82');
5019
+ assert.equal(parsed.searchParams.get('active'), 'true');
5020
+ assert.equal(parsed.searchParams.get('closed'), 'false');
5021
+ } finally {
5022
+ await gamma.close();
5023
+ }
5024
+ });
5025
+
4927
5026
  test('mirror sync accepts --market-address with --dry-run mode alias', async () => {
4928
5027
  const tempDir = createTempDir('pandora-mirror-sync-aliases-');
4929
5028
  const stateFile = path.join(tempDir, 'mirror-state.json');
@@ -1027,6 +1027,124 @@ test('browsePolymarketMarkets filters mock payload deterministically', async ()
1027
1027
  }
1028
1028
  });
1029
1029
 
1030
+ test('browsePolymarketMarkets uses gamma events endpoint for tag-id sports discovery', async () => {
1031
+ const requests = [];
1032
+ const server = http.createServer((req, res) => {
1033
+ requests.push(req.url || '/');
1034
+ const parsed = new URL(req.url || '/', 'http://127.0.0.1');
1035
+ const tagId = parsed.searchParams.get('tag_id');
1036
+
1037
+ res.statusCode = 200;
1038
+ res.setHeader('content-type', 'application/json');
1039
+
1040
+ if (parsed.pathname !== '/events') {
1041
+ res.end(JSON.stringify({ events: [] }));
1042
+ return;
1043
+ }
1044
+
1045
+ if (tagId === '82') {
1046
+ res.end(
1047
+ JSON.stringify({
1048
+ events: [
1049
+ {
1050
+ id: 'evt-82',
1051
+ slug: 'everton-v-burnley',
1052
+ title: 'Everton vs Burnley',
1053
+ markets: [
1054
+ {
1055
+ condition_id: 'sports-c1',
1056
+ market_slug: 'everton-v-burnley-home',
1057
+ question: 'Will Everton beat Burnley?',
1058
+ end_date_iso: '2030-03-09T16:00:00Z',
1059
+ active: true,
1060
+ closed: false,
1061
+ volume24hr: 500000,
1062
+ tokens: [
1063
+ { outcome: 'Yes', price: '0.605', token_id: 'yes-sports-1' },
1064
+ { outcome: 'No', price: '0.395', token_id: 'no-sports-1' },
1065
+ ],
1066
+ },
1067
+ ],
1068
+ },
1069
+ ],
1070
+ }),
1071
+ );
1072
+ return;
1073
+ }
1074
+
1075
+ if (tagId === '100350') {
1076
+ res.end(
1077
+ JSON.stringify({
1078
+ events: [
1079
+ {
1080
+ id: 'evt-100350',
1081
+ slug: 'leeds-v-sunderland',
1082
+ title: 'Leeds vs Sunderland',
1083
+ markets: [
1084
+ {
1085
+ // Duplicate condition id should be deduped across tag-id scans.
1086
+ condition_id: 'sports-c1',
1087
+ market_slug: 'duplicate-market-ignored',
1088
+ question: 'Duplicate row should be ignored',
1089
+ end_date_iso: '2030-03-09T16:00:00Z',
1090
+ active: true,
1091
+ closed: false,
1092
+ volume24hr: 1,
1093
+ tokens: [
1094
+ { outcome: 'Yes', price: '0.5', token_id: 'dup-yes' },
1095
+ { outcome: 'No', price: '0.5', token_id: 'dup-no' },
1096
+ ],
1097
+ },
1098
+ {
1099
+ condition_id: 'sports-c2',
1100
+ market_slug: 'leeds-v-sunderland-home',
1101
+ question: 'Will Leeds beat Sunderland?',
1102
+ end_date_iso: '2030-03-09T16:00:00Z',
1103
+ active: true,
1104
+ closed: false,
1105
+ volume24hr: 400000,
1106
+ tokens: [
1107
+ { outcome: 'Yes', price: '0.495', token_id: 'yes-sports-2' },
1108
+ { outcome: 'No', price: '0.505', token_id: 'no-sports-2' },
1109
+ ],
1110
+ },
1111
+ ],
1112
+ },
1113
+ ],
1114
+ }),
1115
+ );
1116
+ return;
1117
+ }
1118
+
1119
+ res.end(JSON.stringify({ events: [] }));
1120
+ });
1121
+
1122
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
1123
+ const { port } = server.address();
1124
+ const gammaUrl = `http://127.0.0.1:${port}`;
1125
+
1126
+ try {
1127
+ const payload = await browsePolymarketMarkets({
1128
+ gammaUrl,
1129
+ polymarketTagIds: [82, 100350],
1130
+ limit: 10,
1131
+ });
1132
+
1133
+ assert.equal(payload.source, 'polymarket:gamma-events');
1134
+ assert.deepEqual(payload.filters.polymarketTagIds, [82, 100350]);
1135
+ assert.equal(payload.count, 2);
1136
+ assert.equal(payload.items[0].eventSlug, 'everton-v-burnley');
1137
+ assert.equal(payload.items[1].eventSlug, 'leeds-v-sunderland');
1138
+ assert.equal(payload.items[0].eventTitle, 'Everton vs Burnley');
1139
+ assert.equal(payload.items[0].eventId, 'evt-82');
1140
+
1141
+ const eventRequests = requests.filter((entry) => String(entry).startsWith('/events?'));
1142
+ assert.equal(eventRequests.length, 2);
1143
+ } finally {
1144
+ await new Promise((resolve) => server.close(resolve));
1145
+ }
1146
+ });
1147
+
1030
1148
  test('computeApprovalDiff deterministically marks missing allowance/operator checks', () => {
1031
1149
  const payload = computeApprovalDiff({
1032
1150
  ownerAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',