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.
- package/README_FOR_SHARING.md +16 -5
- package/SKILL.md +11 -5
- package/cli/lib/mirror_close_service.cjs +42 -0
- package/cli/lib/mirror_manifest_store.cjs +148 -0
- package/cli/lib/mirror_service.cjs +137 -23
- package/cli/lib/mirror_state_store.cjs +31 -2
- package/cli/lib/mirror_sync_service.cjs +149 -28
- package/cli/lib/mirror_verify_service.cjs +45 -2
- package/cli/lib/pandora_deploy_service.cjs +77 -10
- package/cli/lib/polymarket_trade_adapter.cjs +398 -31
- package/cli/pandora.cjs +1315 -82
- package/package.json +1 -1
package/README_FOR_SHARING.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
166
|
-
- `mirror
|
|
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
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
75
|
-
fs.
|
|
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
|
|