pandora-cli-skills 1.1.7 → 1.1.9

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.
@@ -90,7 +90,7 @@ Prerequisite: Node.js `>=18`.
90
90
  - `pandora export --wallet <0x...> --format csv --year 2026 --out ./trades-2026.csv`
91
91
  - `pandora arbitrage --venues pandora,polymarket --min-spread-pct 3`
92
92
  - `pandora autopilot run|once ...`
93
- - `pandora mirror plan|deploy|verify|sync|status ...`
93
+ - `pandora mirror browse|plan|deploy|verify|go|sync|status|close ...`
94
94
  - `pandora webhook test ...`
95
95
  - `pandora leaderboard --metric profit|volume|win-rate`
96
96
  - `pandora analyze --market-address <0x...> --provider <name>`
@@ -113,10 +113,13 @@ Prerequisite: Node.js `>=18`.
113
113
  - `pandora export --wallet <0x...> --format csv --out ./trades.csv`
114
114
  - `pandora arbitrage --venues pandora,polymarket --min-spread-pct 2 --cross-venue-only --with-rules --include-similarity`
115
115
  - `pandora autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper`
116
+ - `pandora mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
116
117
  - `pandora mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity`
118
+ - `pandora mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper`
117
119
  - `pandora mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity`
118
120
  - `pandora mirror sync once --pandora-market-address <0x...> --polymarket-market-id <id> --paper --hedge-ratio 1.0`
119
- - `pandora mirror status --strategy-hash <hash>`
121
+ - `pandora mirror status --strategy-hash <hash> --with-live`
122
+ - `pandora mirror close --pandora-market-address <0x...> --polymarket-market-id <id> --dry-run`
120
123
  - `pandora webhook test --webhook-url https://example.com/hook`
121
124
  - `pandora leaderboard --metric volume --limit 20`
122
125
  - `pandora analyze --market-address <0x...> --provider mock`
@@ -188,17 +191,25 @@ Prerequisite: Node.js `>=18`.
188
191
  - paper mode is default; live mode requires explicit caps (`--max-amount-usdc`, `--max-open-exposure-usdc`, `--max-trades-per-day`).
189
192
  - `mirror plan`:
190
193
  - envelope is `ok=true`, `command="mirror.plan"`, with `data.sourceMarket`, `data.match`, `data.sizingInputs`, `data.liquidityRecommendation`, and `data.distributionHint`.
194
+ - `mirror browse`:
195
+ - envelope is `ok=true`, `command="mirror.browse"`, with `data.filters`, `data.count`, and `data.items[]`.
191
196
  - `mirror deploy`:
192
- - envelope is `ok=true`, `command="mirror.deploy"`, with `data.planDigest`, `data.deploymentArgs`, `data.tx`, `data.pandora`, and `data.postDeployChecks`.
197
+ - envelope is `ok=true`, `command="mirror.deploy"`, with `data.planDigest`, `data.deploymentArgs`, `data.tx`, `data.preflight`, `data.pandora`, `data.postDeployChecks`, and optional `data.trustManifest`.
193
198
  - `mirror verify`:
194
- - envelope is `ok=true`, `command="mirror.verify"`, with `data.matchConfidence`, `data.ruleHashLeft`, `data.ruleHashRight`, `data.ruleDiffSummary`, `data.similarityChecks[]`, and `data.gateResult`.
199
+ - envelope is `ok=true`, `command="mirror.verify"`, with `data.matchConfidence`, `data.ruleHashLeft`, `data.ruleHashRight`, `data.ruleDiffSummary`, `data.expiry`, optional `data.trustManifest`, `data.similarityChecks[]`, and `data.gateResult`.
200
+ - `mirror go`:
201
+ - envelope is `ok=true`, `command="mirror.go"`, with staged results for `data.plan`, `data.deploy`, `data.verify`, optional `data.sync`, and `data.suggestedSyncCommand`.
195
202
  - `mirror sync`:
196
203
  - envelope is `ok=true`, `command="mirror.sync"`, with `data.strategyHash`, `data.stateFile`, `data.parameters`, `data.snapshots[]`, and `data.actions[]`.
197
204
  - hedge controls: `--hedge-trigger-usdc`, `--max-hedge-usdc`, `--hedge-ratio <n>` (default `1`), and `--no-hedge` to disable hedge execution while keeping drift rebalancing active.
205
+ - trust controls: `--trust-deploy [--manifest-file <path>]`.
206
+ - streaming: `--stream` emits per-tick logs for `mirror sync run` (table mode streams by default).
198
207
  - rebalance sizing is pool-aware: drift notional scales with `reserveYes + reserveNo`, then bounded by `--max-rebalance-usdc`.
199
208
  - Polymarket resilience: when Polymarket endpoints are unreachable, cached snapshots under `~/.pandora/polymarket` are reused for read paths; live sync blocks execution if source data is cached/stale.
200
209
  - `mirror status`:
201
- - envelope is `ok=true`, `command="mirror.status"`, with `data.stateFile`, `data.strategyHash`, and persisted `data.state`.
210
+ - envelope is `ok=true`, `command="mirror.status"`, with `data.stateFile`, `data.strategyHash`, persisted `data.state`, and optional `data.live` when `--with-live` is used.
211
+ - `mirror close`:
212
+ - envelope is `ok=true`, `command="mirror.close"`, with `data.mode` and unwind `data.steps[]` scaffold.
202
213
  - `webhook test`:
203
214
  - envelope is `ok=true`, `command="webhook.test"`, with per-target delivery and retry metadata.
204
215
  - `leaderboard`:
package/SKILL.md CHANGED
@@ -45,7 +45,7 @@ npm link
45
45
  - `pandora export`
46
46
  - `pandora arbitrage`
47
47
  - `pandora autopilot run|once`
48
- - `pandora mirror plan|deploy|verify|sync|status`
48
+ - `pandora mirror browse|plan|deploy|verify|go|sync|status|close`
49
49
  - `pandora webhook test`
50
50
  - `pandora leaderboard`
51
51
  - `pandora analyze`
@@ -98,10 +98,13 @@ pandora --output json history --wallet <0x...> --limit 50
98
98
  pandora --output json export --wallet <0x...> --format csv --out ./trades.csv
99
99
  pandora --output json arbitrage --venues pandora,polymarket --min-spread-pct 3 --cross-venue-only --with-rules --include-similarity
100
100
  pandora --output json autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper
101
+ pandora --output json mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
101
102
  pandora --output json mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity
103
+ pandora --output json mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper
102
104
  pandora --output json mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity
103
105
  pandora --output json mirror sync once --pandora-market-address <0x...> --polymarket-market-id <id> --paper --hedge-ratio 1.0
104
- pandora --output json mirror status --strategy-hash <hash>
106
+ pandora --output json mirror status --strategy-hash <hash> --with-live
107
+ pandora --output json mirror close --pandora-market-address <0x...> --polymarket-market-id <id> --dry-run
105
108
  pandora --output json webhook test --webhook-url https://example.com/hook
106
109
  pandora --output json leaderboard --metric profit --limit 20
107
110
  pandora --output json analyze --market-address <0x...> --provider mock
@@ -162,13 +165,16 @@ pandora --output json suggest --wallet <0x...> --risk medium --budget 50 --inclu
162
165
  - `--include-similarity` includes pairwise similarity diagnostics for agent verification.
163
166
  - `autopilot`: paper-first trigger loop with persisted local state and idempotency.
164
167
  - `mirror plan`: Polymarket mirror sizing plan with liquidity recommendation and distribution hint.
165
- - `mirror deploy`: dry-run/execute Pandora AMM deployment from mirror plan inputs.
166
- - `mirror verify`: explicit question/rules similarity endpoint for AI-subagent validation.
168
+ - `mirror browse`: Polymarket candidate discovery with optional Pandora mirror hints.
169
+ - `mirror deploy`: dry-run/execute Pandora AMM deployment from mirror plan inputs, with execute-time wallet preflight and trust-manifest persistence.
170
+ - `mirror verify`: explicit question/rules similarity endpoint for AI-subagent validation, with optional `--trust-deploy` manifest bypass for similarity.
171
+ - `mirror go`: one-command orchestration for plan → deploy → verify, with optional auto-sync start.
167
172
  - `mirror sync`: paper-first delta-neutral loop with strict gates, state persistence, and optional live hedging (`--hedge-ratio <n>`, `--no-hedge`).
168
173
  - live hedge env: `POLYMARKET_PRIVATE_KEY`, `POLYMARKET_FUNDER`, `POLYMARKET_API_KEY`, `POLYMARKET_API_SECRET`, `POLYMARKET_API_PASSPHRASE`, `POLYMARKET_HOST`.
169
174
  - rebalance sizing is pool-aware and bounded by `--max-rebalance-usdc`.
170
175
  - endpoint resilience: Polymarket snapshots are cached under `~/.pandora/polymarket`; paper/read flows can reuse cache during outages, while live sync blocks cached sources.
171
- - `mirror status`: local mirror state inspection (no network side effects).
176
+ - `mirror status`: local mirror state inspection with optional live market diagnostics (`--with-live`).
177
+ - `mirror close`: deterministic unwind scaffold for LP withdrawal + hedge unwind flow.
172
178
  - `webhook test`: channel validation for generic, Telegram, and Discord payload delivery.
173
179
  - `leaderboard`: ranked user aggregates by profit/volume/win-rate.
174
180
  - invalid indexer aggregates are sanitized (win-rate capped to 0-100%) and emitted in diagnostics.
@@ -0,0 +1,42 @@
1
+ const MIRROR_CLOSE_SCHEMA_VERSION = '1.0.0';
2
+
3
+ function buildMirrorClosePlan(options = {}) {
4
+ const mode = options.execute ? 'execute' : 'dry-run';
5
+ const steps = [
6
+ {
7
+ key: 'pandora-withdraw-lp',
8
+ description: 'Withdraw LP position from Pandora AMM.',
9
+ status: mode === 'execute' ? 'pending' : 'planned',
10
+ },
11
+ {
12
+ key: 'polymarket-unwind-hedge',
13
+ description: 'Close hedge position on Polymarket.',
14
+ status: mode === 'execute' ? 'pending' : 'planned',
15
+ },
16
+ {
17
+ key: 'finalize-report',
18
+ description: 'Compute final mirror unwind report and PnL approximation.',
19
+ status: mode === 'execute' ? 'pending' : 'planned',
20
+ },
21
+ ];
22
+
23
+ return {
24
+ schemaVersion: MIRROR_CLOSE_SCHEMA_VERSION,
25
+ generatedAt: new Date().toISOString(),
26
+ mode,
27
+ pandoraMarketAddress: options.pandoraMarketAddress || null,
28
+ polymarketMarketId: options.polymarketMarketId || null,
29
+ polymarketSlug: options.polymarketSlug || null,
30
+ steps,
31
+ diagnostics: [
32
+ mode === 'execute'
33
+ ? 'Execution path is scaffolded and currently gated.'
34
+ : 'Dry-run close plan generated.',
35
+ ],
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ MIRROR_CLOSE_SCHEMA_VERSION,
41
+ buildMirrorClosePlan,
42
+ };
@@ -0,0 +1,148 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+
6
+ const MIRROR_MANIFEST_SCHEMA_VERSION = '1.0.0';
7
+
8
+ function expandHome(filePath) {
9
+ if (!filePath) return filePath;
10
+ if (filePath === '~') return os.homedir();
11
+ if (filePath.startsWith('~/')) return path.join(os.homedir(), filePath.slice(2));
12
+ return filePath;
13
+ }
14
+
15
+ function defaultManifestFile() {
16
+ return path.join(os.homedir(), '.pandora', 'mirror', 'pairs.json');
17
+ }
18
+
19
+ function resolveManifestPath(filePath) {
20
+ return path.resolve(expandHome(filePath || defaultManifestFile()));
21
+ }
22
+
23
+ function ensureManifestShape(raw) {
24
+ const data = raw && typeof raw === 'object' ? raw : {};
25
+ const pairs = Array.isArray(data.pairs) ? data.pairs : [];
26
+ return {
27
+ schemaVersion: MIRROR_MANIFEST_SCHEMA_VERSION,
28
+ generatedAt: data.generatedAt || new Date().toISOString(),
29
+ pairs,
30
+ };
31
+ }
32
+
33
+ function readManifest(filePath) {
34
+ const resolved = resolveManifestPath(filePath);
35
+ if (!fs.existsSync(resolved)) {
36
+ return {
37
+ filePath: resolved,
38
+ manifest: ensureManifestShape({}),
39
+ };
40
+ }
41
+
42
+ let parsed = {};
43
+ try {
44
+ parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
45
+ } catch {
46
+ parsed = {};
47
+ }
48
+
49
+ return {
50
+ filePath: resolved,
51
+ manifest: ensureManifestShape(parsed),
52
+ };
53
+ }
54
+
55
+ function saveManifest(filePath, manifest) {
56
+ const resolved = resolveManifestPath(filePath);
57
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
58
+ const tmpPath = `${resolved}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`;
59
+ fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
60
+ fs.renameSync(tmpPath, resolved);
61
+ return resolved;
62
+ }
63
+
64
+ function pairMatches(record, selector = {}) {
65
+ const leftMarket = String(record && record.pandoraMarketAddress ? record.pandoraMarketAddress : '').toLowerCase();
66
+ const rightMarket = String(selector && selector.pandoraMarketAddress ? selector.pandoraMarketAddress : '').toLowerCase();
67
+ if (leftMarket && rightMarket && leftMarket !== rightMarket) return false;
68
+
69
+ const leftCondition = String(record && record.polymarketMarketId ? record.polymarketMarketId : '').toLowerCase();
70
+ const rightCondition = String(selector && selector.polymarketMarketId ? selector.polymarketMarketId : '').toLowerCase();
71
+ if (leftCondition && rightCondition && leftCondition !== rightCondition) return false;
72
+
73
+ const leftSlug = String(record && record.polymarketSlug ? record.polymarketSlug : '').toLowerCase();
74
+ const rightSlug = String(selector && selector.polymarketSlug ? selector.polymarketSlug : '').toLowerCase();
75
+ if (leftSlug && rightSlug && leftSlug !== rightSlug) return false;
76
+
77
+ if (!rightMarket && !rightCondition && !rightSlug) return false;
78
+ return true;
79
+ }
80
+
81
+ function findPair(filePath, selector = {}) {
82
+ const loaded = readManifest(filePath);
83
+ const pairs = Array.isArray(loaded.manifest.pairs) ? loaded.manifest.pairs : [];
84
+ const match = pairs.find((record) => pairMatches(record, selector)) || null;
85
+ return {
86
+ filePath: loaded.filePath,
87
+ manifest: loaded.manifest,
88
+ pair: match,
89
+ };
90
+ }
91
+
92
+ function buildPairId(record) {
93
+ const hash = crypto.createHash('sha256');
94
+ hash.update(
95
+ JSON.stringify({
96
+ pandoraMarketAddress: String(record.pandoraMarketAddress || '').toLowerCase(),
97
+ polymarketMarketId: String(record.polymarketMarketId || '').toLowerCase(),
98
+ polymarketSlug: String(record.polymarketSlug || '').toLowerCase(),
99
+ }),
100
+ );
101
+ return hash.digest('hex').slice(0, 16);
102
+ }
103
+
104
+ function upsertPair(filePath, record = {}) {
105
+ const loaded = readManifest(filePath);
106
+ const manifest = loaded.manifest;
107
+ const nowIso = new Date().toISOString();
108
+ const pair = {
109
+ id: record.id || buildPairId(record),
110
+ createdAt: record.createdAt || nowIso,
111
+ updatedAt: nowIso,
112
+ trusted: record.trusted !== false,
113
+ pandoraMarketAddress: record.pandoraMarketAddress || null,
114
+ pandoraPollAddress: record.pandoraPollAddress || null,
115
+ polymarketMarketId: record.polymarketMarketId || null,
116
+ polymarketSlug: record.polymarketSlug || null,
117
+ sourceQuestion: record.sourceQuestion || null,
118
+ sourceRuleHash: record.sourceRuleHash || null,
119
+ };
120
+
121
+ const items = Array.isArray(manifest.pairs) ? manifest.pairs.slice() : [];
122
+ const index = items.findIndex((item) => pairMatches(item, pair));
123
+ if (index >= 0) {
124
+ pair.createdAt = items[index].createdAt || pair.createdAt;
125
+ items[index] = { ...items[index], ...pair };
126
+ } else {
127
+ items.push(pair);
128
+ }
129
+
130
+ manifest.pairs = items;
131
+ manifest.generatedAt = nowIso;
132
+ const savedPath = saveManifest(loaded.filePath, manifest);
133
+ return {
134
+ filePath: savedPath,
135
+ pair,
136
+ manifest,
137
+ };
138
+ }
139
+
140
+ module.exports = {
141
+ MIRROR_MANIFEST_SCHEMA_VERSION,
142
+ defaultManifestFile,
143
+ resolveManifestPath,
144
+ readManifest,
145
+ saveManifest,
146
+ findPair,
147
+ upsertPair,
148
+ };
@@ -2,12 +2,14 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { computeLiquidityRecommendation, computeDistributionHint, normalizeProbability } = require('./mirror_sizing_service.cjs');
5
- const { resolvePolymarketMarket, fetchDepthForMarket } = require('./polymarket_trade_adapter.cjs');
6
- const { findBestPandoraMatch, fetchPandoraMarketContext, verifyMirrorPair } = require('./mirror_verify_service.cjs');
5
+ const { resolvePolymarketMarket, fetchDepthForMarket, browsePolymarketMarkets } = require('./polymarket_trade_adapter.cjs');
6
+ const { findBestPandoraMatch, fetchPandoraMarketContext, verifyMirrorPair, hashRules } = require('./mirror_verify_service.cjs');
7
7
  const { deployPandoraAmmMarket } = require('./pandora_deploy_service.cjs');
8
+ const { defaultManifestFile, upsertPair } = require('./mirror_manifest_store.cjs');
8
9
 
9
10
  const MIRROR_PLAN_SCHEMA_VERSION = '1.0.0';
10
11
  const MIRROR_DEPLOY_SCHEMA_VERSION = '1.0.0';
12
+ const MIRROR_BROWSE_SCHEMA_VERSION = '1.0.0';
11
13
 
12
14
  function round(value, decimals = 6) {
13
15
  const numeric = Number(value);
@@ -20,6 +22,15 @@ function clamp(value, min, max) {
20
22
  return Math.max(min, Math.min(max, value));
21
23
  }
22
24
 
25
+ function createServiceError(code, message, details = undefined) {
26
+ const err = new Error(message);
27
+ err.code = code;
28
+ if (details !== undefined) {
29
+ err.details = details;
30
+ }
31
+ return err;
32
+ }
33
+
23
34
  function normalizeSources(value) {
24
35
  if (!value) return [];
25
36
  if (Array.isArray(value)) {
@@ -68,6 +79,8 @@ async function buildMirrorPlan(options = {}) {
68
79
 
69
80
  const sourceMarket = await resolvePolymarketMarket({
70
81
  host: options.polymarketHost,
82
+ gammaUrl: options.polymarketGammaUrl,
83
+ gammaMockUrl: options.polymarketGammaMockUrl,
71
84
  mockUrl: options.polymarketMockUrl,
72
85
  timeoutMs: options.timeoutMs,
73
86
  marketId: options.polymarketMarketId,
@@ -110,6 +123,14 @@ async function buildMirrorPlan(options = {}) {
110
123
  limit: 150,
111
124
  });
112
125
 
126
+ if (Number.isFinite(sourceMarket.closeTimestamp)) {
127
+ const nowSec = Math.floor(Date.now() / 1000);
128
+ const timeToCloseSec = sourceMarket.closeTimestamp - nowSec;
129
+ if (timeToCloseSec <= 24 * 60 * 60) {
130
+ diagnostics.push(`Source market closes soon (${timeToCloseSec}s). Consider higher monitoring cadence.`);
131
+ }
132
+ }
133
+
113
134
  for (const item of sourceMarket.diagnostics || []) diagnostics.push(item);
114
135
  for (const item of depth.diagnostics || []) diagnostics.push(item);
115
136
  for (const item of sizing.diagnostics || []) diagnostics.push(item);
@@ -214,7 +235,7 @@ function resolveDeployPlanInput(options = {}) {
214
235
  }
215
236
 
216
237
  async function deployMirror(options = {}) {
217
- let planData = resolveDeployPlanInput(options);
238
+ let planData = options.planData || resolveDeployPlanInput(options);
218
239
  if (!planData) {
219
240
  planData = await buildMirrorPlan(options);
220
241
  }
@@ -249,26 +270,63 @@ async function deployMirror(options = {}) {
249
270
  diagnostics.push('Using fallback source URLs because explicit --sources were not provided.');
250
271
  }
251
272
 
252
- const deployPayload = await deployPandoraAmmMarket({
253
- execute: Boolean(options.execute),
254
- chainId: options.chainId,
255
- rpcUrl: options.rpcUrl,
256
- privateKey: options.privateKey,
257
- oracle: options.oracle,
258
- factory: options.factory,
259
- usdc: options.usdc,
260
- question,
261
- rules: sourceRulesText,
262
- sources: sources.length >= 2 ? sources : ['https://polymarket.com', 'https://clob.polymarket.com'],
263
- targetTimestamp,
264
- liquidityUsdc,
265
- distributionYes,
266
- distributionNo,
267
- feeTier: options.feeTier || 3000,
268
- maxImbalance: options.maxImbalance || 10_000,
269
- arbiter: options.arbiter,
270
- category: options.category,
271
- });
273
+ let deployPayload;
274
+ try {
275
+ deployPayload = await deployPandoraAmmMarket({
276
+ execute: Boolean(options.execute),
277
+ chainId: options.chainId,
278
+ rpcUrl: options.rpcUrl,
279
+ privateKey: options.privateKey,
280
+ oracle: options.oracle,
281
+ factory: options.factory,
282
+ usdc: options.usdc,
283
+ question,
284
+ rules: sourceRulesText,
285
+ sources: sources.length >= 2 ? sources : ['https://polymarket.com', 'https://clob.polymarket.com'],
286
+ targetTimestamp,
287
+ minCloseLeadSeconds: Number.isFinite(Number(options.minCloseLeadSeconds))
288
+ ? Number(options.minCloseLeadSeconds)
289
+ : 3600,
290
+ liquidityUsdc,
291
+ distributionYes,
292
+ distributionNo,
293
+ feeTier: options.feeTier || 3000,
294
+ maxImbalance: options.maxImbalance || 10_000,
295
+ arbiter: options.arbiter,
296
+ category: options.category,
297
+ });
298
+ } catch (err) {
299
+ throw createServiceError(
300
+ err && err.code ? err.code : 'MIRROR_DEPLOY_FAILED',
301
+ err && err.message ? err.message : String(err),
302
+ err && err.details ? err.details : undefined,
303
+ );
304
+ }
305
+
306
+ let trustManifest = null;
307
+ if (options.execute && deployPayload.pandora && deployPayload.pandora.marketAddress) {
308
+ const manifestFile = options.manifestFile || defaultManifestFile();
309
+ try {
310
+ const manifestUpdate = upsertPair(manifestFile, {
311
+ trusted: true,
312
+ pandoraMarketAddress: deployPayload.pandora.marketAddress,
313
+ pandoraPollAddress: deployPayload.pandora.pollAddress,
314
+ polymarketMarketId: planData.sourceMarket && planData.sourceMarket.marketId ? String(planData.sourceMarket.marketId) : options.polymarketMarketId || null,
315
+ polymarketSlug: planData.sourceMarket && planData.sourceMarket.slug ? String(planData.sourceMarket.slug) : options.polymarketSlug || null,
316
+ sourceQuestion: planData.sourceMarket && planData.sourceMarket.question ? planData.sourceMarket.question : null,
317
+ sourceRuleHash: hashRules(sourceRulesText),
318
+ });
319
+ trustManifest = {
320
+ filePath: manifestUpdate.filePath,
321
+ pair: manifestUpdate.pair,
322
+ };
323
+ } catch (err) {
324
+ throw createServiceError(
325
+ 'MIRROR_MANIFEST_WRITE_FAILED',
326
+ `Failed to persist mirror trust manifest: ${err && err.message ? err.message : String(err)}`,
327
+ );
328
+ }
329
+ }
272
330
 
273
331
  const postDeployChecks = {
274
332
  seedOddsMatch: null,
@@ -318,6 +376,7 @@ async function deployMirror(options = {}) {
318
376
  tx: deployPayload.tx,
319
377
  pandora: deployPayload.pandora,
320
378
  postDeployChecks,
379
+ trustManifest,
321
380
  diagnostics: diagnostics.concat(deployPayload.diagnostics || []),
322
381
  };
323
382
  }
@@ -326,11 +385,66 @@ async function verifyMirror(options = {}) {
326
385
  return verifyMirrorPair(options);
327
386
  }
328
387
 
388
+ async function browseMirrorMarkets(options = {}) {
389
+ const diagnostics = [];
390
+ const polymarket = await browsePolymarketMarkets({
391
+ gammaUrl: options.polymarketGammaUrl,
392
+ gammaMockUrl: options.polymarketGammaMockUrl,
393
+ mockUrl: options.polymarketMockUrl,
394
+ timeoutMs: options.timeoutMs,
395
+ minYesPct: options.minYesPct,
396
+ maxYesPct: options.maxYesPct,
397
+ minVolume24h: options.minVolume24h,
398
+ closesAfter: options.closesAfter,
399
+ closesBefore: options.closesBefore,
400
+ questionContains: options.questionContains,
401
+ limit: options.limit,
402
+ });
403
+
404
+ const items = [];
405
+ for (const entry of polymarket.items || []) {
406
+ let existingMirror = null;
407
+ if (options.indexerUrl) {
408
+ const match = await findBestPandoraMatch({
409
+ indexerUrl: options.indexerUrl,
410
+ timeoutMs: options.timeoutMs,
411
+ chainId: options.chainId,
412
+ sourceQuestion: entry.question,
413
+ limit: 100,
414
+ });
415
+ if (match.best && match.best.similarity && Number(match.best.similarity.score) >= 0.86) {
416
+ existingMirror = {
417
+ marketAddress: match.best.marketAddress,
418
+ similarity: match.best.similarity.score,
419
+ };
420
+ }
421
+ diagnostics.push(...(match.diagnostics || []));
422
+ }
423
+
424
+ items.push({
425
+ ...entry,
426
+ existingMirror,
427
+ });
428
+ }
429
+
430
+ return {
431
+ schemaVersion: MIRROR_BROWSE_SCHEMA_VERSION,
432
+ generatedAt: new Date().toISOString(),
433
+ source: polymarket.source,
434
+ filters: polymarket.filters,
435
+ count: items.length,
436
+ items,
437
+ diagnostics: (polymarket.diagnostics || []).concat(diagnostics),
438
+ };
439
+ }
440
+
329
441
  module.exports = {
330
442
  MIRROR_PLAN_SCHEMA_VERSION,
331
443
  MIRROR_DEPLOY_SCHEMA_VERSION,
444
+ MIRROR_BROWSE_SCHEMA_VERSION,
332
445
  buildMirrorPlan,
333
446
  deployMirror,
334
447
  verifyMirror,
448
+ browseMirrorMarkets,
335
449
  buildPlanDigest,
336
450
  };
@@ -34,11 +34,23 @@ function ensureStateShape(raw, hash) {
34
34
  schemaVersion: MIRROR_STATE_SCHEMA_VERSION,
35
35
  strategyHash: resolvedHash,
36
36
  startedAt: data.startedAt || new Date().toISOString(),
37
+ pandoraMarketAddress: data.pandoraMarketAddress || null,
38
+ polymarketMarketId: data.polymarketMarketId || null,
39
+ polymarketSlug: data.polymarketSlug || null,
37
40
  lastTickAt: data.lastTickAt || null,
38
41
  lastResetDay: data.lastResetDay || new Date().toISOString().slice(0, 10),
39
42
  tradesToday: Number.isFinite(Number(data.tradesToday)) ? Number(data.tradesToday) : 0,
40
43
  dailySpendUsdc: Number.isFinite(Number(data.dailySpendUsdc)) ? Number(data.dailySpendUsdc) : 0,
41
44
  currentHedgeUsdc: Number.isFinite(Number(data.currentHedgeUsdc)) ? Number(data.currentHedgeUsdc) : 0,
45
+ cumulativeLpFeesApproxUsdc: Number.isFinite(Number(data.cumulativeLpFeesApproxUsdc))
46
+ ? Number(data.cumulativeLpFeesApproxUsdc)
47
+ : 0,
48
+ cumulativeHedgeNotionalUsdc: Number.isFinite(Number(data.cumulativeHedgeNotionalUsdc))
49
+ ? Number(data.cumulativeHedgeNotionalUsdc)
50
+ : 0,
51
+ cumulativeHedgeCostApproxUsdc: Number.isFinite(Number(data.cumulativeHedgeCostApproxUsdc))
52
+ ? Number(data.cumulativeHedgeCostApproxUsdc)
53
+ : 0,
42
54
  lastExecution: data.lastExecution || null,
43
55
  idempotencyKeys: Array.isArray(data.idempotencyKeys) ? data.idempotencyKeys : [],
44
56
  alerts: Array.isArray(data.alerts) ? data.alerts : [],
@@ -71,8 +83,25 @@ function saveState(filePath, state) {
71
83
  const resolved = path.resolve(expandHome(filePath));
72
84
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
73
85
  const tmpPath = `${resolved}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`;
74
- fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
75
- fs.renameSync(tmpPath, resolved);
86
+ const serialized = JSON.stringify(state, null, 2);
87
+ fs.writeFileSync(tmpPath, serialized);
88
+ try {
89
+ fs.renameSync(tmpPath, resolved);
90
+ } catch (err) {
91
+ if (err && err.code === 'ENOENT') {
92
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
93
+ fs.writeFileSync(resolved, serialized);
94
+ if (fs.existsSync(tmpPath)) {
95
+ try {
96
+ fs.unlinkSync(tmpPath);
97
+ } catch {
98
+ // ignore cleanup failure
99
+ }
100
+ }
101
+ } else {
102
+ throw err;
103
+ }
104
+ }
76
105
  return resolved;
77
106
  }
78
107