polymarket-toolkit-mcp 0.7.0
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 +51 -0
- package/dist/cli.js +994 -0
- package/dist/server.js +21303 -0
- package/docs/data/polymarket-api-rate-limits.json +40 -0
- package/package.json +51 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
// ../src/index.ts
|
|
2
|
+
var LB_API_BASE = "https://lb-api.polymarket.com";
|
|
3
|
+
var DATA_API_BASE = "https://data-api.polymarket.com";
|
|
4
|
+
var GAMMA_API_BASE = "https://gamma-api.polymarket.com";
|
|
5
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
6
|
+
async function pmGetJson(url) {
|
|
7
|
+
const ctrl = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, {
|
|
11
|
+
signal: ctrl.signal,
|
|
12
|
+
headers: { accept: "application/json" }
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
const body = await res.text().catch(() => "");
|
|
16
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
17
|
+
}
|
|
18
|
+
return await res.json();
|
|
19
|
+
} finally {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function fetchLbProfitForAddress(address, window = "all") {
|
|
24
|
+
const u = new URL(`${LB_API_BASE}/profit`);
|
|
25
|
+
u.searchParams.set("address", address);
|
|
26
|
+
u.searchParams.set("window", window);
|
|
27
|
+
return pmGetJson(u);
|
|
28
|
+
}
|
|
29
|
+
async function fetchLbProfitLeaderboardPage(options) {
|
|
30
|
+
const u = new URL(`${LB_API_BASE}/profit`);
|
|
31
|
+
u.searchParams.set("window", options.window ?? "all");
|
|
32
|
+
u.searchParams.set("limit", String(options.limit ?? 500));
|
|
33
|
+
u.searchParams.set("offset", String(options.offset ?? 0));
|
|
34
|
+
return pmGetJson(u);
|
|
35
|
+
}
|
|
36
|
+
async function resolveLbUsernameToProxyWallet(username, maxPages = 4) {
|
|
37
|
+
const needle = username.trim().toLowerCase();
|
|
38
|
+
if (!needle) return null;
|
|
39
|
+
for (let page = 0; page < maxPages; page++) {
|
|
40
|
+
const rows = await fetchLbProfitLeaderboardPage({
|
|
41
|
+
limit: 500,
|
|
42
|
+
offset: page * 500
|
|
43
|
+
});
|
|
44
|
+
if (!rows.length) break;
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
const n = (row.name ?? "").toLowerCase();
|
|
47
|
+
const p = (row.pseudonym ?? "").toLowerCase();
|
|
48
|
+
if (n === needle || p === needle) {
|
|
49
|
+
return row.proxyWallet ?? null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (rows.length < 500) break;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async function fetchDataLeaderboardPnL(options) {
|
|
57
|
+
const u = new URL(`${DATA_API_BASE}/v1/leaderboard`);
|
|
58
|
+
u.searchParams.set("timePeriod", options.timePeriod ?? "all");
|
|
59
|
+
u.searchParams.set("orderBy", options.orderBy ?? "PNL");
|
|
60
|
+
u.searchParams.set("category", options.category ?? "overall");
|
|
61
|
+
u.searchParams.set("limit", String(options.limit ?? 50));
|
|
62
|
+
u.searchParams.set("offset", String(options.offset ?? 0));
|
|
63
|
+
return pmGetJson(u);
|
|
64
|
+
}
|
|
65
|
+
async function fetchPositionsPage(user, options = {}) {
|
|
66
|
+
const u = new URL(`${DATA_API_BASE}/positions`);
|
|
67
|
+
u.searchParams.set("user", user);
|
|
68
|
+
u.searchParams.set("sizeThreshold", String(options.sizeThreshold ?? 0));
|
|
69
|
+
u.searchParams.set("limit", String(options.limit ?? 100));
|
|
70
|
+
u.searchParams.set("offset", String(options.offset ?? 0));
|
|
71
|
+
return pmGetJson(u);
|
|
72
|
+
}
|
|
73
|
+
function round6(value) {
|
|
74
|
+
return Math.round(value * 1e6) / 1e6;
|
|
75
|
+
}
|
|
76
|
+
function numeric(value) {
|
|
77
|
+
const n = Number(value ?? 0);
|
|
78
|
+
return Number.isFinite(n) ? n : 0;
|
|
79
|
+
}
|
|
80
|
+
async function fetchRedeemablePositionsPage(user, options = {}) {
|
|
81
|
+
const u = new URL(`${DATA_API_BASE}/positions`);
|
|
82
|
+
u.searchParams.set("user", user);
|
|
83
|
+
u.searchParams.set("redeemable", "true");
|
|
84
|
+
u.searchParams.set("sizeThreshold", "0");
|
|
85
|
+
u.searchParams.set("limit", String(options.limit ?? 100));
|
|
86
|
+
u.searchParams.set("offset", String(options.offset ?? 0));
|
|
87
|
+
return pmGetJson(u);
|
|
88
|
+
}
|
|
89
|
+
function summarizeRedeemablePositions(positions) {
|
|
90
|
+
const byCondition = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const position of positions) {
|
|
92
|
+
if (position.redeemable !== true) continue;
|
|
93
|
+
const conditionId = position.conditionId || "unknown";
|
|
94
|
+
const slug = position.slug || position.eventSlug || position.title || conditionId;
|
|
95
|
+
const raw = position.currentValue == null ? numeric(position.size) : numeric(position.currentValue);
|
|
96
|
+
const estimatedCurrentValue = raw > 0 ? raw : 0;
|
|
97
|
+
const row = byCondition.get(conditionId) ?? {
|
|
98
|
+
conditionId,
|
|
99
|
+
slug,
|
|
100
|
+
count: 0,
|
|
101
|
+
estimatedCurrentValue: 0
|
|
102
|
+
};
|
|
103
|
+
row.count += 1;
|
|
104
|
+
row.estimatedCurrentValue += estimatedCurrentValue;
|
|
105
|
+
byCondition.set(conditionId, row);
|
|
106
|
+
}
|
|
107
|
+
const topConditions = [...byCondition.values()].map((row) => ({ ...row, estimatedCurrentValue: round6(row.estimatedCurrentValue) })).sort((a, b) => b.estimatedCurrentValue - a.estimatedCurrentValue);
|
|
108
|
+
return {
|
|
109
|
+
redeemableCount: topConditions.reduce((total, row) => total + row.count, 0),
|
|
110
|
+
conditionCount: topConditions.length,
|
|
111
|
+
estimatedRedeemableValue: round6(
|
|
112
|
+
topConditions.reduce((total, row) => total + row.estimatedCurrentValue, 0)
|
|
113
|
+
),
|
|
114
|
+
topConditions
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function resolveRedeemMode(options = {}) {
|
|
118
|
+
const mode = (options.mode ?? "").trim().toLowerCase().replace(/-/g, "_");
|
|
119
|
+
if (mode === "low_watermark" || mode === "watermark" || mode === "on_demand") return "low_watermark";
|
|
120
|
+
if (mode === "watchdog" || mode === "watch" || mode === "status" || mode === "dry_run") return "watchdog";
|
|
121
|
+
return typeof options.lowWatermark === "number" && Number.isFinite(options.lowWatermark) && options.lowWatermark > 0 ? "low_watermark" : "watchdog";
|
|
122
|
+
}
|
|
123
|
+
async function fetchActivityPage(user, options = {}) {
|
|
124
|
+
const u = new URL(`${DATA_API_BASE}/activity`);
|
|
125
|
+
u.searchParams.set("user", user);
|
|
126
|
+
u.searchParams.set("limit", String(options.limit ?? 500));
|
|
127
|
+
if (options.end != null) u.searchParams.set("end", String(options.end));
|
|
128
|
+
if (options.type) u.searchParams.set("type", options.type);
|
|
129
|
+
return pmGetJson(u);
|
|
130
|
+
}
|
|
131
|
+
async function fetchGammaEventsBySlug(slug) {
|
|
132
|
+
const u = new URL(`${GAMMA_API_BASE}/events`);
|
|
133
|
+
u.searchParams.set("slug", slug);
|
|
134
|
+
return pmGetJson(u);
|
|
135
|
+
}
|
|
136
|
+
async function fetchGammaMarkets(query = {}) {
|
|
137
|
+
const u = new URL(`${GAMMA_API_BASE}/markets`);
|
|
138
|
+
for (const [k, v] of Object.entries(query)) {
|
|
139
|
+
u.searchParams.set(k, String(v));
|
|
140
|
+
}
|
|
141
|
+
return pmGetJson(u);
|
|
142
|
+
}
|
|
143
|
+
function computeBrierScoreFromSettledPositions(positions) {
|
|
144
|
+
const settled = positions.filter((p) => p.redeemable === true);
|
|
145
|
+
if (!settled.length) return { brier: Number.NaN, n: 0, wins: 0 };
|
|
146
|
+
let sumSq = 0;
|
|
147
|
+
let wins = 0;
|
|
148
|
+
for (const p of settled) {
|
|
149
|
+
const f = Number(p.avgPrice);
|
|
150
|
+
const won = Number(p.currentValue) > 0;
|
|
151
|
+
if (won) wins++;
|
|
152
|
+
const actual = won ? 1 : 0;
|
|
153
|
+
sumSq += (f - actual) ** 2;
|
|
154
|
+
}
|
|
155
|
+
return { brier: sumSq / settled.length, n: settled.length, wins };
|
|
156
|
+
}
|
|
157
|
+
var ACTIVITY_API_ROW_CAP_HINT = 4e3;
|
|
158
|
+
function fingerprintActivityPage(rows) {
|
|
159
|
+
return JSON.stringify(rows);
|
|
160
|
+
}
|
|
161
|
+
function analyzeActivityPaginationWarnings(options) {
|
|
162
|
+
const warnings = [];
|
|
163
|
+
if (options.sawDuplicatePage) {
|
|
164
|
+
warnings.push({
|
|
165
|
+
code: "DUPLICATE_PAGE",
|
|
166
|
+
message: `Activity API returned an identical page twice (reported ~${ACTIVITY_API_ROW_CAP_HINT} row cap). Treat results as INCOMPLETE \u2014 shard by time window or use skills/polymarket-pnl \`pagination_incomplete\` flags.`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (options.sawStaleCursor) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
code: "STALE_CURSOR",
|
|
172
|
+
message: "Activity cursor (`end`) did not advance between pages. Pagination may have stalled."
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (options.sawDuplicatePage || options.sawStaleCursor) {
|
|
176
|
+
warnings.push({
|
|
177
|
+
code: "INCOMPLETE",
|
|
178
|
+
message: "Activity pagination did not produce a complete scan. Shard by time window before relying on totals."
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (options.totalRows >= ACTIVITY_API_ROW_CAP_HINT - 500) {
|
|
182
|
+
warnings.push({
|
|
183
|
+
code: "APPROACHING_CAP",
|
|
184
|
+
message: `Fetched ${options.totalRows} activity rows (approaching ~${ACTIVITY_API_ROW_CAP_HINT} cap). Next pages may repeat JSON.`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return warnings;
|
|
188
|
+
}
|
|
189
|
+
function activityWarningsLikelyIncomplete(warnings) {
|
|
190
|
+
return warnings.some((w) => w.code === "APPROACHING_CAP" || w.code === "INCOMPLETE");
|
|
191
|
+
}
|
|
192
|
+
async function fetchActivityPages(user, options = {}) {
|
|
193
|
+
const limit = options.limit ?? 500;
|
|
194
|
+
const maxPages = options.maxPages ?? 20;
|
|
195
|
+
const collected = [];
|
|
196
|
+
let end;
|
|
197
|
+
let lastFingerprint = "";
|
|
198
|
+
let sawDuplicatePage = false;
|
|
199
|
+
let sawStaleCursor = false;
|
|
200
|
+
let pagesFetched = 0;
|
|
201
|
+
for (let page = 0; page < maxPages; page++) {
|
|
202
|
+
const batch = await fetchActivityPage(user, { limit, end, type: options.type });
|
|
203
|
+
pagesFetched += 1;
|
|
204
|
+
if (!batch.length) break;
|
|
205
|
+
const fp = fingerprintActivityPage(batch);
|
|
206
|
+
if (lastFingerprint && fp === lastFingerprint) {
|
|
207
|
+
sawDuplicatePage = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
lastFingerprint = fp;
|
|
211
|
+
collected.push(...batch);
|
|
212
|
+
if (batch.length < limit) break;
|
|
213
|
+
const lastTs = batch[batch.length - 1]?.timestamp;
|
|
214
|
+
if (lastTs == null) break;
|
|
215
|
+
if (end !== void 0 && end === lastTs) {
|
|
216
|
+
sawStaleCursor = true;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
end = lastTs;
|
|
220
|
+
}
|
|
221
|
+
const warnings = analyzeActivityPaginationWarnings({
|
|
222
|
+
totalRows: collected.length,
|
|
223
|
+
sawDuplicatePage,
|
|
224
|
+
sawStaleCursor
|
|
225
|
+
});
|
|
226
|
+
const likelyIncomplete = activityWarningsLikelyIncomplete(warnings);
|
|
227
|
+
return {
|
|
228
|
+
rows: collected,
|
|
229
|
+
pagesFetched,
|
|
230
|
+
warnings,
|
|
231
|
+
likelyIncomplete
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ../src/cli/util.ts
|
|
236
|
+
var EVM_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
237
|
+
function isEvmAddress(value) {
|
|
238
|
+
return EVM_RE.test(value.trim());
|
|
239
|
+
}
|
|
240
|
+
function normalizeAddress(value) {
|
|
241
|
+
const v = value.trim();
|
|
242
|
+
if (!isEvmAddress(v)) {
|
|
243
|
+
throw new Error(`Invalid EVM address: ${value}`);
|
|
244
|
+
}
|
|
245
|
+
return v.toLowerCase();
|
|
246
|
+
}
|
|
247
|
+
function printJson(value) {
|
|
248
|
+
console.log(JSON.stringify(value, null, 2));
|
|
249
|
+
}
|
|
250
|
+
function usage() {
|
|
251
|
+
console.log(`pm \u2014 Polymarket Toolkit CLI (builders \xB7 public APIs only)
|
|
252
|
+
|
|
253
|
+
Usage:
|
|
254
|
+
pm profile <address|username> [--json] Address snapshot (PnL, positions, Brier)
|
|
255
|
+
pm activity <address> [--type TRADE] Activity pagination + cap warnings
|
|
256
|
+
pm scan [--limit N] [--min-volume N] Market scanner (24h volume \xB7 spread)
|
|
257
|
+
pm updown <event-slug> [--json] Crypto updown Gamma fields / resolution
|
|
258
|
+
pm v2-check [address] V2 split/merge infra checklist (+ activity)
|
|
259
|
+
pm lb [--category overall] [--save] Leaderboard snapshot + diff
|
|
260
|
+
pm lb --diff [--category overall] Diff vs yesterday snapshot
|
|
261
|
+
pm pnl-check <address> LB snapshot + hints; not audit-grade PnL
|
|
262
|
+
pm limits [gamma|data|clob|all] API rate limit pacing table
|
|
263
|
+
pm redeem <address> [low_watermark] Redeem watchdog (read-only)
|
|
264
|
+
pm brier <address|username> [--json] Brier score from settled positions
|
|
265
|
+
pm markets [--limit N] [--active] List Gamma markets (quick scan)
|
|
266
|
+
pm help Show this message
|
|
267
|
+
|
|
268
|
+
Examples:
|
|
269
|
+
pm profile 0x63ce342161250d705dc0b16df89036c8e5f9ba9a
|
|
270
|
+
pm redeem 0x63ce342161250d705dc0b16df89036c8e5f9ba9a 25
|
|
271
|
+
pm markets --limit 5 --active
|
|
272
|
+
|
|
273
|
+
Skills (for AI agents): skills/polymarket-profile \xB7 polymarket-pnl \xB7 polymarket-brier
|
|
274
|
+
Cookbook: docs/cookbook.md \xB7 Examples: examples/
|
|
275
|
+
|
|
276
|
+
No API keys. No signing. Read-only public APIs only.
|
|
277
|
+
`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ../src/cli/commands/activity.ts
|
|
281
|
+
async function resolveAddress(input) {
|
|
282
|
+
const trimmed = input.trim();
|
|
283
|
+
if (isEvmAddress(trimmed)) return normalizeAddress(trimmed);
|
|
284
|
+
const resolved = await resolveLbUsernameToProxyWallet(trimmed);
|
|
285
|
+
if (!resolved) throw new Error(`Could not resolve "${trimmed}" via leaderboard.`);
|
|
286
|
+
return normalizeAddress(resolved);
|
|
287
|
+
}
|
|
288
|
+
async function runActivity(argv) {
|
|
289
|
+
const json = argv.includes("--json");
|
|
290
|
+
const typeIdx = argv.indexOf("--type");
|
|
291
|
+
const activityType = typeIdx >= 0 ? argv[typeIdx + 1] : void 0;
|
|
292
|
+
const limitIdx = argv.indexOf("--limit");
|
|
293
|
+
const limit = limitIdx >= 0 ? Number(argv[limitIdx + 1] ?? 500) : 500;
|
|
294
|
+
const maxPagesIdx = argv.indexOf("--max-pages");
|
|
295
|
+
const maxPages = maxPagesIdx >= 0 ? Number(argv[maxPagesIdx + 1] ?? 10) : 10;
|
|
296
|
+
const skipNext = /* @__PURE__ */ new Set(["--type", "--limit", "--max-pages"]);
|
|
297
|
+
const positional = [];
|
|
298
|
+
for (let i = 0; i < argv.length; i++) {
|
|
299
|
+
const a = argv[i];
|
|
300
|
+
if (a === "--json") continue;
|
|
301
|
+
if (skipNext.has(a)) {
|
|
302
|
+
i++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (a.startsWith("--")) continue;
|
|
306
|
+
positional.push(a);
|
|
307
|
+
}
|
|
308
|
+
const input = positional[0];
|
|
309
|
+
if (!input) {
|
|
310
|
+
throw new Error("usage: pm activity <address|username> [--type TRADE] [--limit N] [--max-pages N] [--json]");
|
|
311
|
+
}
|
|
312
|
+
const address = await resolveAddress(input);
|
|
313
|
+
const result = await fetchActivityPages(address, {
|
|
314
|
+
limit,
|
|
315
|
+
maxPages,
|
|
316
|
+
type: activityType
|
|
317
|
+
});
|
|
318
|
+
const payload = {
|
|
319
|
+
address,
|
|
320
|
+
type: activityType ?? null,
|
|
321
|
+
rowCount: result.rows.length,
|
|
322
|
+
pagesFetched: result.pagesFetched,
|
|
323
|
+
likelyIncomplete: result.likelyIncomplete,
|
|
324
|
+
warnings: result.warnings,
|
|
325
|
+
sample: result.rows.slice(0, 5).map((r) => ({
|
|
326
|
+
type: r.type,
|
|
327
|
+
timestamp: r.timestamp,
|
|
328
|
+
title: r.title?.slice(0, 60),
|
|
329
|
+
usdcSize: r.usdcSize
|
|
330
|
+
}))
|
|
331
|
+
};
|
|
332
|
+
if (json) {
|
|
333
|
+
printJson(payload);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
console.log(`Activity \xB7 ${address}`);
|
|
337
|
+
console.log(` Rows: ${result.rows.length} (${result.pagesFetched} pages)`);
|
|
338
|
+
if (result.warnings.length) {
|
|
339
|
+
for (const w of result.warnings) {
|
|
340
|
+
console.log(` \u26A0\uFE0F [${w.code}] ${w.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (result.likelyIncomplete) {
|
|
344
|
+
console.log(" Status: INCOMPLETE \u2014 do not trust totals beyond this point.");
|
|
345
|
+
}
|
|
346
|
+
for (const row of payload.sample) {
|
|
347
|
+
console.log(` \xB7 ${row.type} ts=${row.timestamp} ${row.title ?? ""} $${row.usdcSize ?? "?"}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ../src/cli/commands/brier.ts
|
|
352
|
+
async function resolveAddress2(input) {
|
|
353
|
+
const trimmed = input.trim();
|
|
354
|
+
if (isEvmAddress(trimmed)) return normalizeAddress(trimmed);
|
|
355
|
+
const resolved = await resolveLbUsernameToProxyWallet(trimmed);
|
|
356
|
+
if (!resolved) throw new Error(`Could not resolve "${trimmed}" via leaderboard.`);
|
|
357
|
+
return normalizeAddress(resolved);
|
|
358
|
+
}
|
|
359
|
+
async function runBrier(argv) {
|
|
360
|
+
const json = argv.includes("--json");
|
|
361
|
+
const input = argv.find((a) => !a.startsWith("--"));
|
|
362
|
+
if (!input) throw new Error("usage: pm brier <address|username> [--json]");
|
|
363
|
+
const address = await resolveAddress2(input);
|
|
364
|
+
const positions = await fetchPositionsPage(address, { limit: 200 });
|
|
365
|
+
const settled = positions.filter((p) => p.redeemable === true);
|
|
366
|
+
const result = computeBrierScoreFromSettledPositions(settled);
|
|
367
|
+
const payload = {
|
|
368
|
+
address,
|
|
369
|
+
brier: Number.isFinite(result.brier) ? result.brier : null,
|
|
370
|
+
settledMarkets: result.n,
|
|
371
|
+
wins: result.wins,
|
|
372
|
+
rating: !Number.isFinite(result.brier) ? "insufficient_sample" : result.brier <= 0.15 ? "good" : result.brier <= 0.25 ? "moderate" : "poor",
|
|
373
|
+
note: "First 200 positions page; full calibration use skills/polymarket-brier."
|
|
374
|
+
};
|
|
375
|
+
if (json) {
|
|
376
|
+
printJson(payload);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
console.log(`Brier \xB7 ${address}`);
|
|
380
|
+
if (!Number.isFinite(result.brier)) {
|
|
381
|
+
console.log(" No settled positions in sample.");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
console.log(` Score: ${result.brier.toFixed(3)} (${payload.rating})`);
|
|
385
|
+
console.log(` Settled: ${result.n} \xB7 Wins: ${result.wins}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ../src/leaderboard.ts
|
|
389
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
390
|
+
import { join } from "node:path";
|
|
391
|
+
function todayUtcDate() {
|
|
392
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
393
|
+
}
|
|
394
|
+
function snapshotPath(baseDir, category, timePeriod, date) {
|
|
395
|
+
return join(baseDir, `${date}_${category}_${timePeriod}.json`);
|
|
396
|
+
}
|
|
397
|
+
function saveSnapshot(baseDir, snapshot) {
|
|
398
|
+
mkdirSync(baseDir, { recursive: true });
|
|
399
|
+
const path = snapshotPath(baseDir, snapshot.category, snapshot.timePeriod, snapshot.date);
|
|
400
|
+
writeFileSync(path, JSON.stringify(snapshot, null, 2));
|
|
401
|
+
return path;
|
|
402
|
+
}
|
|
403
|
+
function loadSnapshot(baseDir, category, timePeriod, date) {
|
|
404
|
+
const path = snapshotPath(baseDir, category, timePeriod, date);
|
|
405
|
+
if (!existsSync(path)) return null;
|
|
406
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
407
|
+
}
|
|
408
|
+
function diffLeaderboardSnapshots(previous, current) {
|
|
409
|
+
const key = (r) => (r.proxyWallet ?? r.userName ?? "").toLowerCase();
|
|
410
|
+
const prevMap = new Map(previous.rows.map((r, i) => [key(r), { row: r, rank: i + 1 }]));
|
|
411
|
+
const currMap = new Map(current.rows.map((r, i) => [key(r), { row: r, rank: i + 1 }]));
|
|
412
|
+
const newFaces = [];
|
|
413
|
+
const dropped = [];
|
|
414
|
+
const rankChanges = [];
|
|
415
|
+
for (const [k, { row, rank }] of currMap) {
|
|
416
|
+
const prev = prevMap.get(k);
|
|
417
|
+
if (!prev) {
|
|
418
|
+
newFaces.push(row);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (prev.rank !== rank) {
|
|
422
|
+
rankChanges.push({
|
|
423
|
+
userName: row.userName ?? k.slice(0, 10),
|
|
424
|
+
proxyWallet: row.proxyWallet ?? "",
|
|
425
|
+
from: prev.rank,
|
|
426
|
+
to: rank,
|
|
427
|
+
pnl: row.pnl != null ? Number(row.pnl) : null
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const [k, { row }] of prevMap) {
|
|
432
|
+
if (!currMap.has(k)) dropped.push(row);
|
|
433
|
+
}
|
|
434
|
+
rankChanges.sort((a, b) => Math.abs(b.from - b.to) - Math.abs(a.from - a.to));
|
|
435
|
+
return { newFaces, dropped, rankChanges };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ../src/cli/commands/lb.ts
|
|
439
|
+
import { join as join2 } from "node:path";
|
|
440
|
+
var SNAPSHOT_DIR = join2(process.cwd(), ".pm-toolkit", "lb-snapshots");
|
|
441
|
+
var CATEGORIES = ["overall", "politics", "crypto", "sports", "weather", "finance", "tech", "culture"];
|
|
442
|
+
async function runLb(argv) {
|
|
443
|
+
const json = argv.includes("--json");
|
|
444
|
+
const save = argv.includes("--save");
|
|
445
|
+
const diff = argv.includes("--diff");
|
|
446
|
+
const catIdx = argv.indexOf("--category");
|
|
447
|
+
const category = catIdx >= 0 ? argv[catIdx + 1] ?? "overall" : "overall";
|
|
448
|
+
const periodIdx = argv.indexOf("--period");
|
|
449
|
+
const timePeriod = periodIdx >= 0 ? argv[periodIdx + 1] ?? "day" : "day";
|
|
450
|
+
const topIdx = argv.indexOf("--top");
|
|
451
|
+
const top = topIdx >= 0 ? Number(argv[topIdx + 1] ?? 10) : 10;
|
|
452
|
+
if (!CATEGORIES.includes(category)) {
|
|
453
|
+
throw new Error(`Unknown category ${category}. Try: ${CATEGORIES.join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
const today = todayUtcDate();
|
|
456
|
+
const yesterday = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
|
|
457
|
+
if (diff) {
|
|
458
|
+
const curr = loadSnapshot(SNAPSHOT_DIR, category, timePeriod, today) ?? await fetchAndBuild(category, timePeriod, top, today);
|
|
459
|
+
const prev = loadSnapshot(SNAPSHOT_DIR, category, timePeriod, yesterday);
|
|
460
|
+
if (!prev) {
|
|
461
|
+
throw new Error(`No snapshot for ${yesterday}. Run: pm lb --category ${category} --save`);
|
|
462
|
+
}
|
|
463
|
+
const delta = diffLeaderboardSnapshots(prev, curr);
|
|
464
|
+
if (json) {
|
|
465
|
+
printJson({ today: curr.date, yesterday: prev.date, category, timePeriod, ...delta });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(`LB diff \xB7 ${category} \xB7 ${timePeriod} \xB7 ${prev.date} \u2192 ${curr.date}
|
|
469
|
+
`);
|
|
470
|
+
console.log(`New faces (${delta.newFaces.length}):`);
|
|
471
|
+
for (const r of delta.newFaces.slice(0, 10)) {
|
|
472
|
+
console.log(` + #${r.rank} ${r.userName} ${r.proxyWallet?.slice(0, 10)} pnl=${r.pnl}`);
|
|
473
|
+
}
|
|
474
|
+
console.log(`
|
|
475
|
+
Rank movers (top ${Math.min(10, delta.rankChanges.length)}):`);
|
|
476
|
+
for (const m of delta.rankChanges.slice(0, 10)) {
|
|
477
|
+
console.log(` ${m.userName}: ${m.from} \u2192 ${m.to} (pnl=${m.pnl})`);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const snapshot = await fetchAndBuild(category, timePeriod, top, today);
|
|
482
|
+
if (save) {
|
|
483
|
+
const path = saveSnapshot(SNAPSHOT_DIR, snapshot);
|
|
484
|
+
console.error(`Saved ${path}`);
|
|
485
|
+
}
|
|
486
|
+
if (json) {
|
|
487
|
+
printJson(snapshot);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
console.log(`Leaderboard \xB7 ${category} \xB7 ${timePeriod} \xB7 top ${top}
|
|
491
|
+
`);
|
|
492
|
+
for (const r of snapshot.rows) {
|
|
493
|
+
console.log(` #${r.rank} ${r.userName ?? "?"} \xB7 pnl=${r.pnl} \xB7 ${r.proxyWallet?.slice(0, 12)}\u2026`);
|
|
494
|
+
}
|
|
495
|
+
console.log(`
|
|
496
|
+
Tip: pm lb --category ${category} --save \xB7 pm lb --diff --category ${category}`);
|
|
497
|
+
}
|
|
498
|
+
async function fetchAndBuild(category, timePeriod, top, date) {
|
|
499
|
+
const rows = await fetchDataLeaderboardPnL({
|
|
500
|
+
limit: top,
|
|
501
|
+
offset: 0,
|
|
502
|
+
timePeriod,
|
|
503
|
+
category,
|
|
504
|
+
orderBy: "PNL"
|
|
505
|
+
});
|
|
506
|
+
return { date, category, timePeriod, rows };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ../src/rate-limits.ts
|
|
510
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
511
|
+
import { dirname, join as join3 } from "node:path";
|
|
512
|
+
import { fileURLToPath } from "node:url";
|
|
513
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
514
|
+
var REGISTRY_PATH = join3(__dirname, "../docs/data/polymarket-api-rate-limits.json");
|
|
515
|
+
var cached = null;
|
|
516
|
+
function loadRateLimitRegistry() {
|
|
517
|
+
if (!cached) {
|
|
518
|
+
cached = JSON.parse(readFileSync2(REGISTRY_PATH, "utf8"));
|
|
519
|
+
}
|
|
520
|
+
return cached;
|
|
521
|
+
}
|
|
522
|
+
function suggestedDelaySeconds(family, scope = "general", safetyFactor = 0.85) {
|
|
523
|
+
const registry = loadRateLimitRegistry();
|
|
524
|
+
const fam = registry.families[family];
|
|
525
|
+
if (!fam) throw new Error(`Unknown API family: ${family}`);
|
|
526
|
+
const match = fam.limits.find((l) => l.scope === scope) ?? fam.limits.find((l) => l.scope === "general");
|
|
527
|
+
if (!match) throw new Error(`No limit for ${family}/${scope}`);
|
|
528
|
+
return Math.round(match.window_seconds / match.requests / safetyFactor * 1e4) / 1e4;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ../src/cli/commands/limits.ts
|
|
532
|
+
async function runLimits(argv) {
|
|
533
|
+
const json = argv.includes("--json");
|
|
534
|
+
const family = argv.find((a) => !a.startsWith("--")) ?? "all";
|
|
535
|
+
const registry = loadRateLimitRegistry();
|
|
536
|
+
if (family === "all") {
|
|
537
|
+
const summary = Object.entries(registry.families).map(([key, fam2]) => ({
|
|
538
|
+
family: key,
|
|
539
|
+
base_url: fam2.base_url,
|
|
540
|
+
scopes: fam2.limits.map((l) => ({
|
|
541
|
+
...l,
|
|
542
|
+
suggested_delay_s: suggestedDelaySeconds(key, l.scope)
|
|
543
|
+
}))
|
|
544
|
+
}));
|
|
545
|
+
if (json) {
|
|
546
|
+
printJson({ updated_at: registry.updated_at, source: registry.source.url, families: summary });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
console.log(`Rate limits \xB7 updated ${registry.updated_at}`);
|
|
550
|
+
console.log(`Source: ${registry.source.url}
|
|
551
|
+
`);
|
|
552
|
+
for (const fam2 of summary) {
|
|
553
|
+
console.log(`${fam2.family} \xB7 ${fam2.base_url}`);
|
|
554
|
+
for (const s of fam2.scopes.slice(0, 4)) {
|
|
555
|
+
console.log(` ${s.scope}: ${s.requests}/${s.window_seconds}s \xB7 sleep ~${s.suggested_delay_s}s`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const fam = registry.families[family];
|
|
561
|
+
if (!fam) throw new Error(`Unknown family. Try: ${Object.keys(registry.families).join(", ")}`);
|
|
562
|
+
if (json) {
|
|
563
|
+
printJson(fam);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
console.log(`${family} \xB7 ${fam.base_url}
|
|
567
|
+
`);
|
|
568
|
+
for (const l of fam.limits) {
|
|
569
|
+
console.log(` ${l.scope}: ${l.requests} req / ${l.window_seconds}s \xB7 delay ~${suggestedDelaySeconds(family, l.scope)}s`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ../src/cli/commands/markets.ts
|
|
574
|
+
async function runMarkets(argv) {
|
|
575
|
+
const limitIdx = argv.indexOf("--limit");
|
|
576
|
+
const limit = limitIdx >= 0 ? Number(argv[limitIdx + 1] ?? 10) : 10;
|
|
577
|
+
const active = argv.includes("--active");
|
|
578
|
+
const rows = await fetchGammaMarkets({
|
|
579
|
+
limit,
|
|
580
|
+
...active ? { active: true, closed: false } : {}
|
|
581
|
+
});
|
|
582
|
+
const mapped = rows.map((m) => ({
|
|
583
|
+
question: m.question,
|
|
584
|
+
slug: m.slug,
|
|
585
|
+
volume: m.volume != null ? Number(m.volume) : null,
|
|
586
|
+
liquidity: m.liquidity != null ? Number(m.liquidity) : null
|
|
587
|
+
}));
|
|
588
|
+
printJson({ count: mapped.length, markets: mapped });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ../src/cli/commands/pnl-check.ts
|
|
592
|
+
async function runPnlCheck(argv) {
|
|
593
|
+
const json = argv.includes("--json");
|
|
594
|
+
const input = argv.find((a) => !a.startsWith("--"));
|
|
595
|
+
if (!input) throw new Error("usage: pm pnl-check <0x-address> [--json]");
|
|
596
|
+
const address = normalizeAddress(input);
|
|
597
|
+
if (!isEvmAddress(address)) throw new Error("Invalid address");
|
|
598
|
+
const [lbRows, tradePage, rebatePage] = await Promise.all([
|
|
599
|
+
fetchLbProfitForAddress(address, "all"),
|
|
600
|
+
fetchActivityPage(address, { limit: 500, type: "TRADE" }),
|
|
601
|
+
fetchActivityPage(address, { limit: 200, type: "MAKER_REBATE" })
|
|
602
|
+
]);
|
|
603
|
+
const lbPnl = lbRows[0]?.amount != null ? Number(lbRows[0].amount) : null;
|
|
604
|
+
const name = lbRows[0]?.name ?? address.slice(0, 10);
|
|
605
|
+
const payload = {
|
|
606
|
+
address,
|
|
607
|
+
name,
|
|
608
|
+
lbPnlAllTime: lbPnl,
|
|
609
|
+
lbSource: "lb-api.polymarket.com/profit?window=all",
|
|
610
|
+
activitySample: {
|
|
611
|
+
tradeRowsFirstPage: tradePage.length,
|
|
612
|
+
makerRebateRowsFirstPage: rebatePage.length
|
|
613
|
+
},
|
|
614
|
+
interpretation: [
|
|
615
|
+
"LB PnL is a quick official snapshot and hint surface; it is not audit-grade PnL.",
|
|
616
|
+
"Position-level profile PnL is approximate.",
|
|
617
|
+
"For cashflow replay run: python3 skills/polymarket-pnl/compute_precise_pnl.py --address \u2026"
|
|
618
|
+
],
|
|
619
|
+
docs: "docs/fee-inclusive-pnl.md"
|
|
620
|
+
};
|
|
621
|
+
if (json) {
|
|
622
|
+
printJson(payload);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
console.log(`PnL check \xB7 ${name} \xB7 ${address}
|
|
626
|
+
`);
|
|
627
|
+
console.log(` LB all-time: ${lbPnl != null ? `$${lbPnl.toLocaleString()}` : "n/a"}`);
|
|
628
|
+
console.log(` Activity sample: ${payload.activitySample.tradeRowsFirstPage} TRADE rows (page 1)`);
|
|
629
|
+
console.log(` Rebates sample: ${payload.activitySample.makerRebateRowsFirstPage} MAKER_REBATE rows (page 1)`);
|
|
630
|
+
console.log(" Scope: LB snapshot + hints; not audit-grade PnL.");
|
|
631
|
+
console.log("\n \u2192 Audit: python3 skills/polymarket-pnl/compute_precise_pnl.py --address " + address);
|
|
632
|
+
console.log(" \u2192 Read: docs/fee-inclusive-pnl.md");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ../src/cli/commands/profile.ts
|
|
636
|
+
async function resolveIdentity(input) {
|
|
637
|
+
const trimmed = input.trim();
|
|
638
|
+
if (isEvmAddress(trimmed)) {
|
|
639
|
+
return { address: normalizeAddress(trimmed), displayName: null };
|
|
640
|
+
}
|
|
641
|
+
const resolved = await resolveLbUsernameToProxyWallet(trimmed);
|
|
642
|
+
if (!resolved) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`Could not resolve "${trimmed}" via leaderboard. Pass a 0x proxy wallet or a ranked username.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
return { address: normalizeAddress(resolved), displayName: trimmed };
|
|
648
|
+
}
|
|
649
|
+
async function lbAmount(address, window) {
|
|
650
|
+
const rows = await fetchLbProfitForAddress(address, window);
|
|
651
|
+
const head = rows[0];
|
|
652
|
+
if (!head || head.amount == null) return null;
|
|
653
|
+
const n = Number(head.amount);
|
|
654
|
+
return Number.isFinite(n) ? n : null;
|
|
655
|
+
}
|
|
656
|
+
function fmtUsd(n) {
|
|
657
|
+
if (n == null) return "n/a";
|
|
658
|
+
return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
659
|
+
}
|
|
660
|
+
async function runProfile(argv) {
|
|
661
|
+
const json = argv.includes("--json");
|
|
662
|
+
const positional = argv.filter((a) => !a.startsWith("--"));
|
|
663
|
+
const input = positional[0];
|
|
664
|
+
if (!input) throw new Error("usage: pm profile <address|username> [--json]");
|
|
665
|
+
const { address, displayName } = await resolveIdentity(input);
|
|
666
|
+
const [pnlAll, pnl7d, pnl30d, positionsRaw, lbMeta] = await Promise.all([
|
|
667
|
+
lbAmount(address, "all"),
|
|
668
|
+
lbAmount(address, "7d"),
|
|
669
|
+
lbAmount(address, "30d"),
|
|
670
|
+
fetchPositionsPage(address, { limit: 100 }),
|
|
671
|
+
fetchLbProfitForAddress(address, "all")
|
|
672
|
+
]);
|
|
673
|
+
const positions = positionsRaw;
|
|
674
|
+
const open = positions.filter((p) => p.redeemable !== true);
|
|
675
|
+
const settledForBrier = positions.filter((p) => p.redeemable === true);
|
|
676
|
+
const brier = computeBrierScoreFromSettledPositions(settledForBrier);
|
|
677
|
+
const unrealized = open.reduce((sum, p) => sum + Number(p.currentValue ?? 0), 0);
|
|
678
|
+
const topOpen = [...open].sort((a, b) => Math.abs(Number(b.currentValue ?? 0)) - Math.abs(Number(a.currentValue ?? 0))).slice(0, 5);
|
|
679
|
+
const name = displayName ?? lbMeta[0]?.name ?? lbMeta[0]?.pseudonym ?? address.slice(0, 10);
|
|
680
|
+
const payload = {
|
|
681
|
+
address,
|
|
682
|
+
displayName: name,
|
|
683
|
+
pnl: { all: pnlAll, d7: pnl7d, d30: pnl30d, source: "lb-api.polymarket.com/profit" },
|
|
684
|
+
positions: {
|
|
685
|
+
openCount: open.length,
|
|
686
|
+
settledSampleCount: settledForBrier.length,
|
|
687
|
+
unrealizedValue: unrealized,
|
|
688
|
+
topOpen: topOpen.map((p) => ({
|
|
689
|
+
title: p.title ?? p.slug ?? "unknown",
|
|
690
|
+
currentValue: Number(p.currentValue ?? 0),
|
|
691
|
+
cashPnl: Number(p.cashPnl ?? 0)
|
|
692
|
+
}))
|
|
693
|
+
},
|
|
694
|
+
brier: {
|
|
695
|
+
score: Number.isFinite(brier.brier) ? brier.brier : null,
|
|
696
|
+
settledMarkets: brier.n,
|
|
697
|
+
wins: brier.wins,
|
|
698
|
+
note: "First 100 positions page only; use polymarket-pnl skill for audit-grade PnL."
|
|
699
|
+
},
|
|
700
|
+
limitations: [
|
|
701
|
+
"LB PnL only; 7d/30d may be empty for inactive accounts.",
|
|
702
|
+
"Positions capped at first API page (100 rows).",
|
|
703
|
+
"For full profile + categories use skills/polymarket-profile."
|
|
704
|
+
]
|
|
705
|
+
};
|
|
706
|
+
if (json) {
|
|
707
|
+
printJson(payload);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
|
|
711
|
+
console.log(` Polymarket Profile \xB7 ${name}`);
|
|
712
|
+
console.log(` ${address}`);
|
|
713
|
+
console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
|
|
714
|
+
console.log(`
|
|
715
|
+
\u{1F4CA} PnL (LB API)`);
|
|
716
|
+
console.log(` All-time: ${fmtUsd(pnlAll)}`);
|
|
717
|
+
console.log(` 7d: ${fmtUsd(pnl7d)}`);
|
|
718
|
+
console.log(` 30d: ${fmtUsd(pnl30d)}`);
|
|
719
|
+
console.log(`
|
|
720
|
+
\u{1F4C2} Positions (first page)`);
|
|
721
|
+
console.log(` Open: ${open.length} \xB7 Unrealized \u2248 ${fmtUsd(unrealized)}`);
|
|
722
|
+
for (const p of payload.positions.topOpen) {
|
|
723
|
+
console.log(` \xB7 ${p.title.slice(0, 48)} \u2014 ${fmtUsd(p.currentValue)}`);
|
|
724
|
+
}
|
|
725
|
+
if (brier.n > 0) {
|
|
726
|
+
console.log(`
|
|
727
|
+
\u{1F3AF} Brier (settled in sample)`);
|
|
728
|
+
console.log(` Score: ${brier.brier.toFixed(3)} \xB7 ${brier.wins}/${brier.n} winning markets`);
|
|
729
|
+
}
|
|
730
|
+
console.log(`
|
|
731
|
+
\u26A0\uFE0F ${payload.limitations[2]}`);
|
|
732
|
+
console.log(` Audit PnL: skills/polymarket-pnl`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ../src/cli/commands/redeem.ts
|
|
736
|
+
async function runRedeem(argv) {
|
|
737
|
+
const address = argv[0];
|
|
738
|
+
if (!address) throw new Error("usage: pm redeem <address> [low_watermark]");
|
|
739
|
+
const lowWatermark = argv[1] ? Number(argv[1]) : void 0;
|
|
740
|
+
const user = normalizeAddress(address);
|
|
741
|
+
const rows = await fetchRedeemablePositionsPage(user, { limit: 100, offset: 0 });
|
|
742
|
+
const summary = summarizeRedeemablePositions(
|
|
743
|
+
rows
|
|
744
|
+
);
|
|
745
|
+
const mode = resolveRedeemMode({ lowWatermark });
|
|
746
|
+
printJson({
|
|
747
|
+
user,
|
|
748
|
+
mode,
|
|
749
|
+
lowWatermark: lowWatermark ?? null,
|
|
750
|
+
...summary,
|
|
751
|
+
topConditions: summary.topConditions.slice(0, 5)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ../src/scanner.ts
|
|
756
|
+
function num(value) {
|
|
757
|
+
const n = Number(value ?? 0);
|
|
758
|
+
return Number.isFinite(n) ? n : 0;
|
|
759
|
+
}
|
|
760
|
+
function numOrNull(value) {
|
|
761
|
+
if (value == null || value === "") return null;
|
|
762
|
+
const n = Number(value);
|
|
763
|
+
return Number.isFinite(n) ? n : null;
|
|
764
|
+
}
|
|
765
|
+
function rankMarketsForScan(markets, options = {}) {
|
|
766
|
+
const minVol = options.minVolume24hr ?? 0;
|
|
767
|
+
const limit = options.limit ?? 20;
|
|
768
|
+
const rows = [];
|
|
769
|
+
for (const m of markets) {
|
|
770
|
+
if (m.closed === true || m.active === false) continue;
|
|
771
|
+
const volume24hr = num(m.volume24hrClob ?? m.volume24hr ?? m.volumeNum);
|
|
772
|
+
if (volume24hr < minVol) continue;
|
|
773
|
+
rows.push({
|
|
774
|
+
slug: m.slug ?? "unknown",
|
|
775
|
+
question: (m.question ?? m.slug ?? "unknown").slice(0, 120),
|
|
776
|
+
volume24hr,
|
|
777
|
+
spread: numOrNull(m.spread),
|
|
778
|
+
liquidity: num(m.liquidityNum ?? m.liquidity),
|
|
779
|
+
bestBid: numOrNull(m.bestBid),
|
|
780
|
+
bestAsk: numOrNull(m.bestAsk),
|
|
781
|
+
acceptingOrders: m.acceptingOrders !== false
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
rows.sort((a, b) => b.volume24hr - a.volume24hr || (a.spread ?? 999) - (b.spread ?? 999));
|
|
785
|
+
return rows.slice(0, limit);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ../src/cli/commands/scan.ts
|
|
789
|
+
async function runScan(argv) {
|
|
790
|
+
const json = argv.includes("--json");
|
|
791
|
+
const limitIdx = argv.indexOf("--limit");
|
|
792
|
+
const limit = limitIdx >= 0 ? Number(argv[limitIdx + 1] ?? 20) : 20;
|
|
793
|
+
const minVolIdx = argv.indexOf("--min-volume");
|
|
794
|
+
const minVolume = minVolIdx >= 0 ? Number(argv[minVolIdx + 1] ?? 0) : 1e3;
|
|
795
|
+
const raw = await fetchGammaMarkets({
|
|
796
|
+
limit: 150,
|
|
797
|
+
active: true,
|
|
798
|
+
closed: false
|
|
799
|
+
});
|
|
800
|
+
const ranked = rankMarketsForScan(raw, { minVolume24hr: minVolume, limit });
|
|
801
|
+
const payload = {
|
|
802
|
+
count: ranked.length,
|
|
803
|
+
minVolume24hr: minVolume,
|
|
804
|
+
markets: ranked,
|
|
805
|
+
note: "Sorted by 24h volume \xB7 read-only Gamma snapshot"
|
|
806
|
+
};
|
|
807
|
+
if (json) {
|
|
808
|
+
printJson(payload);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
console.log(`Market scan \xB7 top ${ranked.length} by 24h volume (min $${minVolume})
|
|
812
|
+
`);
|
|
813
|
+
for (const m of ranked) {
|
|
814
|
+
const spread = m.spread != null ? `${(m.spread * 100).toFixed(2)}%` : "n/a";
|
|
815
|
+
console.log(
|
|
816
|
+
` $${m.volume24hr.toLocaleString()} vol \xB7 spread ${spread} \xB7 ${m.question.slice(0, 55)}`
|
|
817
|
+
);
|
|
818
|
+
console.log(` slug: ${m.slug}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ../src/cli/commands/updown.ts
|
|
823
|
+
async function runUpdown(argv) {
|
|
824
|
+
const json = argv.includes("--json");
|
|
825
|
+
const slug = argv.find((a) => !a.startsWith("--"));
|
|
826
|
+
if (!slug) {
|
|
827
|
+
throw new Error("usage: pm updown <event-slug> [--json]\n example: pm updown btc-updown-15m-1779796800");
|
|
828
|
+
}
|
|
829
|
+
const events = await fetchGammaEventsBySlug(slug);
|
|
830
|
+
const event = events[0];
|
|
831
|
+
if (!event) {
|
|
832
|
+
throw new Error(`No Gamma event for slug: ${slug}`);
|
|
833
|
+
}
|
|
834
|
+
const markets = (event.markets ?? []).map((m) => ({
|
|
835
|
+
question: m.question,
|
|
836
|
+
slug: m.slug,
|
|
837
|
+
conditionId: m.conditionId,
|
|
838
|
+
groupItemTitle: m.groupItemTitle,
|
|
839
|
+
outcomePrices: m.outcomePrices,
|
|
840
|
+
bestBid: m.bestBid,
|
|
841
|
+
bestAsk: m.bestAsk,
|
|
842
|
+
spread: m.spread,
|
|
843
|
+
resolutionSource: m.resolutionSource ?? event.resolutionSource,
|
|
844
|
+
endDate: m.endDate ?? event.endDate
|
|
845
|
+
}));
|
|
846
|
+
const payload = {
|
|
847
|
+
slug,
|
|
848
|
+
title: event.title,
|
|
849
|
+
endDate: event.endDate,
|
|
850
|
+
resolutionSource: event.resolutionSource,
|
|
851
|
+
marketCount: markets.length,
|
|
852
|
+
markets,
|
|
853
|
+
pitfalls: [
|
|
854
|
+
"No universal priceToBeat field \u2014 verify resolutionSource + official rules",
|
|
855
|
+
"Do not use CLOB mid as oracle target without recording your own window open",
|
|
856
|
+
"See docs/crypto-updown-price-source.md"
|
|
857
|
+
]
|
|
858
|
+
};
|
|
859
|
+
if (json) {
|
|
860
|
+
printJson(payload);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
console.log(`${event.title ?? slug}`);
|
|
864
|
+
console.log(` end: ${event.endDate ?? "n/a"}`);
|
|
865
|
+
console.log(` resolutionSource: ${(event.resolutionSource ?? "see market page").slice(0, 120)}`);
|
|
866
|
+
console.log(` markets: ${markets.length}
|
|
867
|
+
`);
|
|
868
|
+
for (const m of markets.slice(0, 8)) {
|
|
869
|
+
console.log(` \xB7 ${m.groupItemTitle ?? m.question?.slice(0, 50)}`);
|
|
870
|
+
console.log(` prices: ${m.outcomePrices} \xB7 bid/ask: ${m.bestBid}/${m.bestAsk} \xB7 spread: ${m.spread}`);
|
|
871
|
+
}
|
|
872
|
+
if (markets.length > 8) console.log(` \u2026 +${markets.length - 8} more`);
|
|
873
|
+
console.log("\n\u26A0\uFE0F Read docs/crypto-updown-price-source.md before trading on derived target price.");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ../src/cli/commands/v2-check.ts
|
|
877
|
+
var CHECKLIST = [
|
|
878
|
+
"CTF contract unchanged (0x4D97\u20266045) \u2014 split/merge target",
|
|
879
|
+
"Exchange contract = V2 (not legacy V1 address in your config)",
|
|
880
|
+
"CLOB client: @polymarket/clob-client-v2 for V2 order path",
|
|
881
|
+
"Relayer host: relayer-v2.polymarket.com (if using gasless)",
|
|
882
|
+
"Collateral: pUSD / wrap path matches your env (post cutover)",
|
|
883
|
+
"Per-market negRisk flag \u2192 correct adapter (merge/split path)",
|
|
884
|
+
"CTF setApprovalForAll(adapter + exchange) after wallet migration",
|
|
885
|
+
"Order body: no stale nonce field (V2 SDK)",
|
|
886
|
+
"Separate CLOB auth errors from on-chain merge reverts"
|
|
887
|
+
];
|
|
888
|
+
async function runV2Check(argv) {
|
|
889
|
+
const json = argv.includes("--json");
|
|
890
|
+
const input = argv.find((a) => !a.startsWith("--") && a !== "v2-check");
|
|
891
|
+
const payload = {
|
|
892
|
+
checklist: CHECKLIST,
|
|
893
|
+
docs: "docs/v2-ctf-ops-faq.md",
|
|
894
|
+
note: "Read-only checklist. On-chain execution is out of scope for this repo."
|
|
895
|
+
};
|
|
896
|
+
if (input) {
|
|
897
|
+
const address = isEvmAddress(input) ? normalizeAddress(input) : null;
|
|
898
|
+
if (!address) {
|
|
899
|
+
throw new Error("Optional address must be 0x\u2026 proxy wallet");
|
|
900
|
+
}
|
|
901
|
+
const [merges, splits, conversions] = await Promise.all([
|
|
902
|
+
fetchActivityPages(address, { limit: 200, maxPages: 3, type: "MERGE" }),
|
|
903
|
+
fetchActivityPages(address, { limit: 200, maxPages: 3, type: "SPLIT" }),
|
|
904
|
+
fetchActivityPages(address, { limit: 200, maxPages: 3, type: "CONVERSION" })
|
|
905
|
+
]);
|
|
906
|
+
const lastTs = (rows) => {
|
|
907
|
+
const arr = rows;
|
|
908
|
+
return arr.length ? arr[arr.length - 1]?.timestamp ?? null : null;
|
|
909
|
+
};
|
|
910
|
+
payload.address = address;
|
|
911
|
+
payload.recentActivity = {
|
|
912
|
+
merge: { count: merges.rows.length, lastTimestamp: lastTs(merges.rows), warnings: merges.warnings },
|
|
913
|
+
split: { count: splits.rows.length, lastTimestamp: lastTs(splits.rows), warnings: splits.warnings },
|
|
914
|
+
conversion: {
|
|
915
|
+
count: conversions.rows.length,
|
|
916
|
+
lastTimestamp: lastTs(conversions.rows),
|
|
917
|
+
warnings: conversions.warnings
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (json) {
|
|
922
|
+
printJson(payload);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
console.log("V2 / CTF readiness checklist (builder \xB7 read-only)\n");
|
|
926
|
+
CHECKLIST.forEach((line, i) => console.log(` ${i + 1}. ${line}`));
|
|
927
|
+
console.log(`
|
|
928
|
+
Full FAQ: docs/v2-ctf-ops-faq.md`);
|
|
929
|
+
if (payload.recentActivity) {
|
|
930
|
+
console.log(`
|
|
931
|
+
Recent on-chain activity sample \xB7 ${payload.address}`);
|
|
932
|
+
const ra = payload.recentActivity;
|
|
933
|
+
for (const [k, v] of Object.entries(ra)) {
|
|
934
|
+
console.log(` ${k.toUpperCase()}: ${v.count} rows (sample) \xB7 last ts=${v.lastTimestamp ?? "n/a"}`);
|
|
935
|
+
}
|
|
936
|
+
console.log(" \u2192 If MERGE count=0 after cutover but SPLIT>0, suspect infra drift (see FAQ).");
|
|
937
|
+
} else {
|
|
938
|
+
console.log("\nTip: ./bin/pm v2-check 0xYourProxy \u2014 attach MERGE/SPLIT activity sample");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ../src/cli/main.ts
|
|
943
|
+
async function main() {
|
|
944
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
945
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
946
|
+
usage();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
switch (cmd) {
|
|
950
|
+
case "profile":
|
|
951
|
+
await runProfile(rest);
|
|
952
|
+
break;
|
|
953
|
+
case "redeem":
|
|
954
|
+
await runRedeem(rest);
|
|
955
|
+
break;
|
|
956
|
+
case "activity":
|
|
957
|
+
await runActivity(rest);
|
|
958
|
+
break;
|
|
959
|
+
case "brier":
|
|
960
|
+
await runBrier(rest);
|
|
961
|
+
break;
|
|
962
|
+
case "markets":
|
|
963
|
+
await runMarkets(rest);
|
|
964
|
+
break;
|
|
965
|
+
case "scan":
|
|
966
|
+
await runScan(rest);
|
|
967
|
+
break;
|
|
968
|
+
case "updown":
|
|
969
|
+
await runUpdown(rest);
|
|
970
|
+
break;
|
|
971
|
+
case "v2-check":
|
|
972
|
+
await runV2Check(rest);
|
|
973
|
+
break;
|
|
974
|
+
case "lb":
|
|
975
|
+
await runLb(rest);
|
|
976
|
+
break;
|
|
977
|
+
case "pnl-check":
|
|
978
|
+
await runPnlCheck(rest);
|
|
979
|
+
break;
|
|
980
|
+
case "limits":
|
|
981
|
+
await runLimits(rest);
|
|
982
|
+
break;
|
|
983
|
+
default:
|
|
984
|
+
console.error(`Unknown command: ${cmd}
|
|
985
|
+
`);
|
|
986
|
+
usage();
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
main().catch((err) => {
|
|
991
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
992
|
+
console.error(`Error: ${message}`);
|
|
993
|
+
process.exit(1);
|
|
994
|
+
});
|