@blockrun/franklin 3.15.73 → 3.15.75
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/dist/agent/context.js +1 -1
- package/dist/tools/prediction.debug.js +828 -0
- package/dist/tools/prediction.js +175 -54
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -336,7 +336,7 @@ Your training data is frozen in the past. Live-world questions MUST be answered
|
|
|
336
336
|
|
|
337
337
|
If you find yourself about to emit one of these, stop and call the tool instead. If you don't know which ticker the user means, call ExaSearch or AskUser — never deflect.
|
|
338
338
|
|
|
339
|
-
**Prediction markets (PredictionMarket).** When the user asks about real-world odds — elections, "will X happen by year-end", "Polymarket on Y", "Kalshi market for Z", "what are the odds of recession" — use **PredictionMarket** instead of guessing.
|
|
339
|
+
**Prediction markets (PredictionMarket).** When the user asks about real-world odds — elections, "will X happen by year-end", "Polymarket on Y", "Kalshi market for Z", "what are the odds of recession" — use **PredictionMarket** instead of guessing. Ten actions, route by intent:
|
|
340
340
|
- "is there a market on X anywhere?" / unknown which platform → \`searchAll\` (\$0.005) — single call across Polymarket+Kalshi+Limitless+Opinion+Predict.Fun.
|
|
341
341
|
- "what are the odds on Polymarket / Kalshi specifically" → \`searchPolymarket\` (\$0.001) and \`searchKalshi\` (\$0.001) **in parallel**; comparing implied probability across the two venues is the high-value answer.
|
|
342
342
|
- "where do Polymarket and Kalshi disagree / arbitrage" → \`crossPlatform\` (\$0.005) returns pre-matched pairs.
|
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PredictionMarket — unified access to Polymarket / Kalshi / Limitless /
|
|
3
|
+
* Opinion / Predict.Fun / cross-platform / smart-money / wallet endpoints
|
|
4
|
+
* via the BlockRun gateway. Each call settles via x402 against the user's
|
|
5
|
+
* USDC wallet.
|
|
6
|
+
*
|
|
7
|
+
* Powered server-side by Predexon; surfaced to the agent as a single
|
|
8
|
+
* action-dispatched tool so the inventory stays small. Keep one cohesive
|
|
9
|
+
* tool — the way TradingMarket bundles 6 actions — instead of forty
|
|
10
|
+
* one-shot capabilities, otherwise weak models start hallucinating tool
|
|
11
|
+
* names.
|
|
12
|
+
*
|
|
13
|
+
* searchAll $0.005 search markets across Polymarket+Kalshi+
|
|
14
|
+
* Limitless+Opinion+Predict.Fun in one call
|
|
15
|
+
* searchPolymarket $0.001 query Polymarket markets (event filter, sort)
|
|
16
|
+
* searchKalshi $0.001 query Kalshi markets
|
|
17
|
+
* crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
|
|
18
|
+
* (the arbitrage / consensus signal)
|
|
19
|
+
* leaderboard $0.001 global Polymarket leaderboard — top wallets by P&L
|
|
20
|
+
* walletProfile $0.005 full Polymarket wallet profile (single wallet)
|
|
21
|
+
* or batch profiles (comma-separated wallets)
|
|
22
|
+
* walletPnl $0.005 P&L summary + realized P&L time series for one
|
|
23
|
+
* Polymarket wallet
|
|
24
|
+
* walletPositions $0.005 open + historical positions for one Polymarket
|
|
25
|
+
* wallet
|
|
26
|
+
* smartActivity $0.005 discover markets where high-performing wallets
|
|
27
|
+
* are active right now
|
|
28
|
+
* smartMoney $0.005 smart-money positioning on one Polymarket
|
|
29
|
+
* condition_id (per-market drill-down)
|
|
30
|
+
*
|
|
31
|
+
* Output is filtered + truncated on the way back so a single call never
|
|
32
|
+
* dumps 100 markets into the agent's context. Default 20 rows; agents that
|
|
33
|
+
* need more should narrow the search.
|
|
34
|
+
*/
|
|
35
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
36
|
+
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
37
|
+
import { logger } from '../logger.js';
|
|
38
|
+
import { recordFetch } from '../trading/providers/telemetry.js';
|
|
39
|
+
const TIMEOUT_MS = 30_000;
|
|
40
|
+
const DEFAULT_LIMIT = 20;
|
|
41
|
+
const MAX_LIMIT = 50;
|
|
42
|
+
// Per-action price table — mirrors the Predexon openapi.json. Used to feed
|
|
43
|
+
// the Markets-tab telemetry ring buffer so prediction-market spend appears
|
|
44
|
+
// in "Calls today / Spend today / Recent paid calls" alongside trading calls.
|
|
45
|
+
// If a path isn't here we don't record cost — we still record the fetch
|
|
46
|
+
// (success/latency) so panel health stays accurate.
|
|
47
|
+
const PATH_PRICES = [
|
|
48
|
+
{ pattern: /\/v1\/pm\/markets\/search$/, usd: 0.005 },
|
|
49
|
+
{ pattern: /\/v1\/pm\/matching-markets/, usd: 0.005 },
|
|
50
|
+
{ pattern: /\/v1\/pm\/polymarket\/wallets\//, usd: 0.005 },
|
|
51
|
+
{ pattern: /\/v1\/pm\/polymarket\/wallet\//, usd: 0.005 },
|
|
52
|
+
{ pattern: /\/v1\/pm\/polymarket\/market\/[^/]+\/smart-money$/, usd: 0.005 },
|
|
53
|
+
{ pattern: /\/v1\/pm\/polymarket\/markets\/smart-activity$/, usd: 0.005 },
|
|
54
|
+
{ pattern: /\/v1\/pm\/.+/, usd: 0.001 },
|
|
55
|
+
];
|
|
56
|
+
function priceForPath(path) {
|
|
57
|
+
for (const { pattern, usd } of PATH_PRICES) {
|
|
58
|
+
if (pattern.test(path))
|
|
59
|
+
return usd;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
// ─── Shared GET-with-x402 flow ────────────────────────────────────────────
|
|
64
|
+
async function getWithPayment(path, query, ctx) {
|
|
65
|
+
const chain = loadChain();
|
|
66
|
+
const apiUrl = API_URLS[chain];
|
|
67
|
+
const qs = new URLSearchParams();
|
|
68
|
+
for (const [k, v] of Object.entries(query)) {
|
|
69
|
+
if (v == null || v === '')
|
|
70
|
+
continue;
|
|
71
|
+
qs.set(k, String(v));
|
|
72
|
+
}
|
|
73
|
+
const queryStr = qs.toString();
|
|
74
|
+
const endpoint = `${apiUrl}${path}${queryStr ? `?${queryStr}` : ''}`;
|
|
75
|
+
const headers = {
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
'User-Agent': `franklin/${VERSION}`,
|
|
78
|
+
};
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
81
|
+
const onAbort = () => controller.abort();
|
|
82
|
+
ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
83
|
+
const startedAt = Date.now();
|
|
84
|
+
let costRecorded = 0;
|
|
85
|
+
try {
|
|
86
|
+
let response = await fetch(endpoint, { method: 'GET', signal: controller.signal, headers });
|
|
87
|
+
if (response.status === 402) {
|
|
88
|
+
const paymentHeaders = await signPayment(response, chain, endpoint);
|
|
89
|
+
if (!paymentHeaders) {
|
|
90
|
+
throw new Error('Payment signing failed — check wallet balance');
|
|
91
|
+
}
|
|
92
|
+
response = await fetch(endpoint, {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
headers: { ...headers, ...paymentHeaders },
|
|
96
|
+
});
|
|
97
|
+
// Only record cost on the post-402 settlement; the initial 402
|
|
98
|
+
// response is free and counting it would double-charge the panel.
|
|
99
|
+
costRecorded = priceForPath(path);
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const errText = await response.text().catch(() => '');
|
|
103
|
+
// Surface failed paid calls in the Markets-tab health summary.
|
|
104
|
+
recordFetch({ provider: 'blockrun', endpoint: path, ok: false, latencyMs: Date.now() - startedAt });
|
|
105
|
+
throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 600)}`);
|
|
106
|
+
}
|
|
107
|
+
recordFetch({
|
|
108
|
+
provider: 'blockrun',
|
|
109
|
+
endpoint: path,
|
|
110
|
+
ok: true,
|
|
111
|
+
latencyMs: Date.now() - startedAt,
|
|
112
|
+
costUsd: costRecorded > 0 ? costRecorded : undefined,
|
|
113
|
+
});
|
|
114
|
+
return (await response.json());
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
ctx.abortSignal.removeEventListener('abort', onAbort);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function signPayment(response, chain, endpoint) {
|
|
122
|
+
try {
|
|
123
|
+
const paymentHeader = await extractPaymentReq(response);
|
|
124
|
+
if (!paymentHeader)
|
|
125
|
+
return null;
|
|
126
|
+
if (chain === 'solana') {
|
|
127
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
128
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
129
|
+
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
130
|
+
const secretBytes = await solanaKeyToBytes(wallet.privateKey);
|
|
131
|
+
const feePayer = details.extra?.feePayer || details.recipient;
|
|
132
|
+
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
133
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
134
|
+
resourceDescription: details.resource?.description || 'Franklin PredictionMarket call',
|
|
135
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
136
|
+
extra: details.extra,
|
|
137
|
+
});
|
|
138
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
139
|
+
}
|
|
140
|
+
const wallet = await getOrCreateWallet();
|
|
141
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
142
|
+
const details = extractPaymentDetails(paymentRequired);
|
|
143
|
+
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
144
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
145
|
+
resourceDescription: details.resource?.description || 'Franklin PredictionMarket call',
|
|
146
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 60,
|
|
147
|
+
extra: details.extra,
|
|
148
|
+
});
|
|
149
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
logger.warn(`[franklin] PredictionMarket payment error: ${err.message}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function extractPaymentReq(response) {
|
|
157
|
+
let header = response.headers.get('payment-required');
|
|
158
|
+
if (!header) {
|
|
159
|
+
try {
|
|
160
|
+
const body = (await response.json());
|
|
161
|
+
if (body.x402 || body.accepts)
|
|
162
|
+
header = btoa(JSON.stringify(body));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* ignore */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return header;
|
|
169
|
+
}
|
|
170
|
+
// ─── Formatting helpers ────────────────────────────────────────────────────
|
|
171
|
+
function asNumber(value) {
|
|
172
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
173
|
+
return value;
|
|
174
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
175
|
+
const n = Number(value);
|
|
176
|
+
if (Number.isFinite(n))
|
|
177
|
+
return n;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function formatUsd(value) {
|
|
182
|
+
const n = asNumber(value);
|
|
183
|
+
if (n == null)
|
|
184
|
+
return 'n/a';
|
|
185
|
+
if (n >= 1e9)
|
|
186
|
+
return `$${(n / 1e9).toFixed(2)}B`;
|
|
187
|
+
if (n >= 1e6)
|
|
188
|
+
return `$${(n / 1e6).toFixed(2)}M`;
|
|
189
|
+
if (n >= 1e3)
|
|
190
|
+
return `$${(n / 1e3).toFixed(1)}K`;
|
|
191
|
+
return `$${n.toFixed(2)}`;
|
|
192
|
+
}
|
|
193
|
+
function formatQuantity(value) {
|
|
194
|
+
const n = asNumber(value);
|
|
195
|
+
if (n == null)
|
|
196
|
+
return String(value ?? 'n/a');
|
|
197
|
+
return Number.isInteger(n) ? n.toLocaleString() : n.toLocaleString(undefined, { maximumFractionDigits: 4 });
|
|
198
|
+
}
|
|
199
|
+
function formatPct(value, digits = 1) {
|
|
200
|
+
const n = asNumber(value);
|
|
201
|
+
if (n == null)
|
|
202
|
+
return 'n/a';
|
|
203
|
+
const pct = Math.abs(n) > 1 ? n : n * 100;
|
|
204
|
+
return `${pct.toFixed(digits)}%`;
|
|
205
|
+
}
|
|
206
|
+
// API responses sometimes come wrapped as `{data: [...], pagination: ...}`,
|
|
207
|
+
// other times as a bare array. Normalise to an array.
|
|
208
|
+
function unwrapList(raw) {
|
|
209
|
+
if (Array.isArray(raw))
|
|
210
|
+
return raw;
|
|
211
|
+
if (raw && typeof raw === 'object') {
|
|
212
|
+
const obj = raw;
|
|
213
|
+
if (Array.isArray(obj.data))
|
|
214
|
+
return obj.data;
|
|
215
|
+
if (Array.isArray(obj.markets))
|
|
216
|
+
return obj.markets;
|
|
217
|
+
if (Array.isArray(obj.pairs))
|
|
218
|
+
return obj.pairs;
|
|
219
|
+
if (Array.isArray(obj.results))
|
|
220
|
+
return obj.results;
|
|
221
|
+
if (Array.isArray(obj.positions))
|
|
222
|
+
return obj.positions;
|
|
223
|
+
}
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
function parseWalletsInput(value) {
|
|
227
|
+
return value
|
|
228
|
+
.split(',')
|
|
229
|
+
.map(w => w.trim())
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pick the first usable string from a list of candidate values.
|
|
234
|
+
*
|
|
235
|
+
* Predexon responses sometimes wrap titles/labels inside nested objects
|
|
236
|
+
* (e.g. `position.market = { slug, question, title }` instead of a flat
|
|
237
|
+
* `position.title`). Pre-3.15.75 the formatter `as string` cast these
|
|
238
|
+
* objects and ended up rendering `[object Object]` for every position
|
|
239
|
+
* row — verified 2026-05-06 in a real session.
|
|
240
|
+
*
|
|
241
|
+
* Strategy:
|
|
242
|
+
* - string → return as-is (after trim)
|
|
243
|
+
* - object → walk a small set of common name-bearing keys
|
|
244
|
+
* (title, question, slug, name, label, market_slug) and return the
|
|
245
|
+
* first one that yields a string
|
|
246
|
+
* - anything else (number / array / null) → skip
|
|
247
|
+
* - all candidates exhausted → undefined
|
|
248
|
+
*/
|
|
249
|
+
function pickString(...candidates) {
|
|
250
|
+
const NAME_KEYS = ['title', 'question', 'slug', 'name', 'label', 'market_slug', 'event_title'];
|
|
251
|
+
for (const c of candidates) {
|
|
252
|
+
if (typeof c === 'string') {
|
|
253
|
+
const trimmed = c.trim();
|
|
254
|
+
if (trimmed)
|
|
255
|
+
return trimmed;
|
|
256
|
+
}
|
|
257
|
+
else if (c && typeof c === 'object' && !Array.isArray(c)) {
|
|
258
|
+
const obj = c;
|
|
259
|
+
for (const k of NAME_KEYS) {
|
|
260
|
+
const v = obj[k];
|
|
261
|
+
if (typeof v === 'string' && v.trim())
|
|
262
|
+
return v.trim();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
async function execute(input, ctx) {
|
|
269
|
+
const { action, search, status, sort, limit, conditionId, wallets, granularity } = input;
|
|
270
|
+
const cappedLimit = Math.min(Math.max(1, limit ?? DEFAULT_LIMIT), MAX_LIMIT);
|
|
271
|
+
if (!action) {
|
|
272
|
+
return {
|
|
273
|
+
output: 'Error: action is required (searchAll | searchPolymarket | searchKalshi | crossPlatform | leaderboard | walletProfile | walletPnl | walletPositions | smartActivity | smartMoney)',
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
switch (action) {
|
|
279
|
+
case 'searchAll': {
|
|
280
|
+
// One $0.005 call across 5 platforms — Polymarket, Kalshi, Limitless,
|
|
281
|
+
// Opinion, Predict.Fun. The right entry point for "is there a market
|
|
282
|
+
// on X anywhere?" — beats firing per-platform searches in parallel.
|
|
283
|
+
// Predexon expects `q` for the search term — verified 2026-05-06 from
|
|
284
|
+
// a live 422: {"detail":[{"type":"missing","loc":["query","q"]}]}.
|
|
285
|
+
// Public input field stays `search` for ergonomic consistency with
|
|
286
|
+
// searchPolymarket / searchKalshi; rename on the wire.
|
|
287
|
+
const raw = await getWithPayment('/v1/pm/markets/search', {
|
|
288
|
+
q: search,
|
|
289
|
+
status,
|
|
290
|
+
sort,
|
|
291
|
+
limit: cappedLimit,
|
|
292
|
+
}, ctx);
|
|
293
|
+
// Predexon returns either a flat list or per-platform buckets.
|
|
294
|
+
// Try the bucket shape first; fall back to a flat list.
|
|
295
|
+
const lines = [
|
|
296
|
+
`## Cross-platform market search` + (search ? ` · "${search}"` : ''),
|
|
297
|
+
'_Searched Polymarket, Kalshi, Limitless, Opinion, Predict.Fun in one call._',
|
|
298
|
+
'',
|
|
299
|
+
];
|
|
300
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
301
|
+
const obj = raw;
|
|
302
|
+
const platforms = ['polymarket', 'kalshi', 'limitless', 'opinion', 'predictfun', 'predict_fun'];
|
|
303
|
+
let totalShown = 0;
|
|
304
|
+
for (const p of platforms) {
|
|
305
|
+
const list = unwrapList(obj[p]);
|
|
306
|
+
if (list.length === 0)
|
|
307
|
+
continue;
|
|
308
|
+
const remaining = cappedLimit - totalShown;
|
|
309
|
+
if (remaining <= 0)
|
|
310
|
+
break;
|
|
311
|
+
const shown = list.slice(0, Math.min(5, remaining));
|
|
312
|
+
lines.push(`### ${p}`);
|
|
313
|
+
shown.forEach((m, i) => {
|
|
314
|
+
const title = pickString(m.title, m.question, m.market, m.event, m.market_slug, m.slug, m.ticker) ?? 'untitled';
|
|
315
|
+
const id = pickString(m.condition_id, m.ticker, m.id);
|
|
316
|
+
const idTag = id ? ` · \`${String(id).slice(0, 18)}…\`` : '';
|
|
317
|
+
const vol = m.volume != null ? ` · vol ${formatUsd(m.volume)}` : '';
|
|
318
|
+
lines.push(`${i + 1}. ${title}${idTag}${vol}`);
|
|
319
|
+
totalShown++;
|
|
320
|
+
});
|
|
321
|
+
lines.push('');
|
|
322
|
+
}
|
|
323
|
+
if (totalShown === 0) {
|
|
324
|
+
// Bucket shape but empty — fall back to flat-list interpretation.
|
|
325
|
+
const flat = unwrapList(raw);
|
|
326
|
+
if (flat.length === 0) {
|
|
327
|
+
return { output: 'No markets matched across any platform.' };
|
|
328
|
+
}
|
|
329
|
+
flat.slice(0, cappedLimit).forEach((m, i) => {
|
|
330
|
+
const title = pickString(m.title, m.question, m.market, m.event, m.market_slug, m.slug, m.ticker) ?? 'untitled';
|
|
331
|
+
const platform = pickString(m.platform, m.source) ?? 'unknown';
|
|
332
|
+
lines.push(`${i + 1}. **[${platform}]** ${title}`);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const flat = unwrapList(raw);
|
|
338
|
+
if (flat.length === 0) {
|
|
339
|
+
return { output: 'No markets matched across any platform.' };
|
|
340
|
+
}
|
|
341
|
+
flat.slice(0, cappedLimit).forEach((m, i) => {
|
|
342
|
+
const title = (m.title || m.question || m.market_slug || m.ticker || 'untitled');
|
|
343
|
+
const platform = (m.platform || m.source || 'unknown');
|
|
344
|
+
lines.push(`${i + 1}. **[${platform}]** ${title}`);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
lines.push(`_$0.005 paid via x402._`);
|
|
348
|
+
return { output: lines.join('\n') };
|
|
349
|
+
}
|
|
350
|
+
case 'leaderboard': {
|
|
351
|
+
// Global top-wallet ranking. Cheap ($0.001) — the right answer to
|
|
352
|
+
// "who's making money on Polymarket" / "who should I follow".
|
|
353
|
+
const raw = await getWithPayment('/v1/pm/polymarket/leaderboard', {
|
|
354
|
+
limit: cappedLimit,
|
|
355
|
+
sort,
|
|
356
|
+
}, ctx);
|
|
357
|
+
const rows = unwrapList(raw);
|
|
358
|
+
if (rows.length === 0) {
|
|
359
|
+
return { output: 'No leaderboard data returned.' };
|
|
360
|
+
}
|
|
361
|
+
const lines = [
|
|
362
|
+
`## Polymarket leaderboard — top ${rows.length} wallet${rows.length === 1 ? '' : 's'}`,
|
|
363
|
+
'',
|
|
364
|
+
];
|
|
365
|
+
rows.forEach((r, i) => {
|
|
366
|
+
const wallet = pickString(r.wallet, r.address, r.proxy_wallet, r.proxyWallet) ?? 'unknown';
|
|
367
|
+
const w = wallet.length > 12
|
|
368
|
+
? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
|
|
369
|
+
: wallet;
|
|
370
|
+
const pnl = r.pnl ?? r.realized_pnl ?? r.total_pnl;
|
|
371
|
+
const volume = r.volume ?? r.total_volume;
|
|
372
|
+
const winRate = r.win_rate ?? r.winRate;
|
|
373
|
+
const name = pickString(r.name, r.handle, r.username);
|
|
374
|
+
const handle = name ? ` (${name})` : '';
|
|
375
|
+
const parts = [];
|
|
376
|
+
if (pnl != null)
|
|
377
|
+
parts.push(`P&L ${formatUsd(pnl)}`);
|
|
378
|
+
if (volume != null)
|
|
379
|
+
parts.push(`vol ${formatUsd(volume)}`);
|
|
380
|
+
if (winRate != null)
|
|
381
|
+
parts.push(`win ${formatPct(winRate, 0)}`);
|
|
382
|
+
lines.push(`${i + 1}. \`${w}\`${handle}` + (parts.length > 0 ? ` — ${parts.join(' · ')}` : ''));
|
|
383
|
+
});
|
|
384
|
+
lines.push('', `_$0.001 paid via x402._`);
|
|
385
|
+
return { output: lines.join('\n') };
|
|
386
|
+
}
|
|
387
|
+
case 'walletProfile': {
|
|
388
|
+
if (!wallets || !wallets.trim()) {
|
|
389
|
+
return {
|
|
390
|
+
output: 'Error: `wallets` is required for walletProfile (single address or comma-separated list of Polymarket wallet addresses)',
|
|
391
|
+
isError: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Smart dispatch: a single wallet → /wallet/{addr} (full profile,
|
|
395
|
+
// labels, scores, stats); a comma-list → /wallets/profiles (batch).
|
|
396
|
+
// The 3.15.70 ship hit the BATCH endpoint for everything and got 422
|
|
397
|
+
// for the single-wallet case; the gateway team confirmed 2026-05-06
|
|
398
|
+
// the right surface for "analyze this trader" is the path-parameter
|
|
399
|
+
// single-wallet endpoint, not the batch query-param one.
|
|
400
|
+
const parsedWallets = parseWalletsInput(wallets);
|
|
401
|
+
if (parsedWallets.length === 0) {
|
|
402
|
+
return {
|
|
403
|
+
output: 'Error: `wallets` must include at least one Polymarket wallet address',
|
|
404
|
+
isError: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const list = parsedWallets.join(',');
|
|
408
|
+
const isBatch = parsedWallets.length > 1;
|
|
409
|
+
const raw = isBatch
|
|
410
|
+
? await getWithPayment('/v1/pm/polymarket/wallets/profiles', {
|
|
411
|
+
addresses: list,
|
|
412
|
+
}, ctx)
|
|
413
|
+
: await getWithPayment(`/v1/pm/polymarket/wallet/${encodeURIComponent(list)}`, {}, ctx);
|
|
414
|
+
// Single-wallet path returns a single profile object; batch returns
|
|
415
|
+
// an array (or {data:[]}). unwrapList handles the batch shape but
|
|
416
|
+
// returns [] for a bare object — wrap explicitly so the formatter
|
|
417
|
+
// below sees the single profile.
|
|
418
|
+
if (process.env.FRANKLIN_PM_DEBUG === '1') process.stderr.write('[pm-debug] walletProfile raw: ' + JSON.stringify(raw).slice(0,1500) + '\n');
|
|
419
|
+
const profiles = isBatch
|
|
420
|
+
? unwrapList(raw)
|
|
421
|
+
: (raw && typeof raw === 'object' ? [raw] : []);
|
|
422
|
+
if (profiles.length === 0) {
|
|
423
|
+
return { output: `No profile data returned for: ${wallets}` };
|
|
424
|
+
}
|
|
425
|
+
const lines = [
|
|
426
|
+
`## Polymarket wallet profile${profiles.length === 1 ? '' : 's'} — ${profiles.length}`,
|
|
427
|
+
'',
|
|
428
|
+
];
|
|
429
|
+
profiles.forEach((p, i) => {
|
|
430
|
+
const wallet = pickString(p.wallet, p.address, p.proxy_wallet, p.proxyWallet) ?? 'unknown';
|
|
431
|
+
const w = wallet.length > 12
|
|
432
|
+
? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
|
|
433
|
+
: wallet;
|
|
434
|
+
const name = pickString(p.name, p.handle, p.username);
|
|
435
|
+
const pnl = p.pnl ?? p.realized_pnl ?? p.total_pnl;
|
|
436
|
+
const unrealized = p.unrealized_pnl;
|
|
437
|
+
const volume = p.volume ?? p.total_volume;
|
|
438
|
+
const positions = p.positions_count ?? p.open_positions;
|
|
439
|
+
const winRate = p.win_rate ?? p.winRate;
|
|
440
|
+
lines.push(`${i + 1}. \`${w}\`` + (name ? ` (${name})` : ''));
|
|
441
|
+
const stats = [];
|
|
442
|
+
if (pnl != null)
|
|
443
|
+
stats.push(`P&L ${formatUsd(pnl)}`);
|
|
444
|
+
if (unrealized != null)
|
|
445
|
+
stats.push(`unrealized ${formatUsd(unrealized)}`);
|
|
446
|
+
if (volume != null)
|
|
447
|
+
stats.push(`vol ${formatUsd(volume)}`);
|
|
448
|
+
if (positions != null)
|
|
449
|
+
stats.push(`${positions} open`);
|
|
450
|
+
if (winRate != null)
|
|
451
|
+
stats.push(`win ${formatPct(winRate, 0)}`);
|
|
452
|
+
if (stats.length > 0)
|
|
453
|
+
lines.push(` ${stats.join(' · ')}`);
|
|
454
|
+
});
|
|
455
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
456
|
+
return { output: lines.join('\n') };
|
|
457
|
+
}
|
|
458
|
+
case 'walletPnl': {
|
|
459
|
+
// Single-wallet P&L summary + time series.
|
|
460
|
+
// Predexon path: /v1/pm/polymarket/wallet/pnl/{wallet} — Tier 2 ($0.005).
|
|
461
|
+
if (!wallets || !wallets.trim()) {
|
|
462
|
+
return {
|
|
463
|
+
output: 'Error: `wallets` is required for walletPnl (single Polymarket wallet address)',
|
|
464
|
+
isError: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const parsedWallets = parseWalletsInput(wallets);
|
|
468
|
+
if (parsedWallets.length !== 1) {
|
|
469
|
+
return {
|
|
470
|
+
output: 'Error: walletPnl accepts exactly one wallet address. For multiple wallets, call walletPnl once per address in parallel.',
|
|
471
|
+
isError: true,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const wallet = parsedWallets[0];
|
|
475
|
+
// Predexon requires `granularity` from the enum {day, week, month,
|
|
476
|
+
// year, all} — verified 2026-05-06 in two live 422 turns. Default
|
|
477
|
+
// `day`; agent can override via input field for longer aggregations.
|
|
478
|
+
const raw = await getWithPayment(`/v1/pm/polymarket/wallet/pnl/${encodeURIComponent(wallet)}`, { granularity: granularity ?? 'day' }, ctx);
|
|
479
|
+
if (process.env.FRANKLIN_PM_DEBUG === '1') process.stderr.write('[pm-debug] walletPnl raw: ' + JSON.stringify(raw).slice(0,1500) + '\n');
|
|
480
|
+
if (!raw || typeof raw !== 'object') {
|
|
481
|
+
return { output: `No P&L data returned for ${wallet}` };
|
|
482
|
+
}
|
|
483
|
+
const data = raw;
|
|
484
|
+
const realized = data.realized_pnl ?? data.realizedPnl ?? data.total_pnl ?? data.pnl;
|
|
485
|
+
const unrealized = data.unrealized_pnl ?? data.unrealizedPnl;
|
|
486
|
+
const total = data.total_value ?? data.totalValue ?? data.equity;
|
|
487
|
+
const volume = data.volume ?? data.total_volume;
|
|
488
|
+
const winRate = data.win_rate ?? data.winRate;
|
|
489
|
+
const w = wallet.length > 12 ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}` : wallet;
|
|
490
|
+
const lines = [`## Polymarket wallet P&L — \`${w}\``, ''];
|
|
491
|
+
const summary = [];
|
|
492
|
+
if (realized != null)
|
|
493
|
+
summary.push(`realized ${formatUsd(realized)}`);
|
|
494
|
+
if (unrealized != null)
|
|
495
|
+
summary.push(`unrealized ${formatUsd(unrealized)}`);
|
|
496
|
+
if (total != null)
|
|
497
|
+
summary.push(`equity ${formatUsd(total)}`);
|
|
498
|
+
if (volume != null)
|
|
499
|
+
summary.push(`vol ${formatUsd(volume)}`);
|
|
500
|
+
if (winRate != null)
|
|
501
|
+
summary.push(`win ${formatPct(winRate, 0)}`);
|
|
502
|
+
if (summary.length > 0)
|
|
503
|
+
lines.push(summary.join(' · '));
|
|
504
|
+
// Optional time series — show recent points compactly if present.
|
|
505
|
+
const series = (data.series ?? data.history ?? data.daily);
|
|
506
|
+
if (Array.isArray(series) && series.length > 0) {
|
|
507
|
+
lines.push('', `**Recent points** (latest ${Math.min(7, series.length)}):`);
|
|
508
|
+
series.slice(-7).forEach(pt => {
|
|
509
|
+
const t = (pt.date ?? pt.ts ?? pt.timestamp);
|
|
510
|
+
const v = (pt.pnl ?? pt.value ?? pt.cumulative_pnl);
|
|
511
|
+
if (t != null && v != null) {
|
|
512
|
+
const tStr = typeof t === 'number' ? new Date(t).toISOString().slice(0, 10) : String(t).slice(0, 10);
|
|
513
|
+
lines.push(`- ${tStr} · ${formatUsd(v)}`);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
518
|
+
return { output: lines.join('\n') };
|
|
519
|
+
}
|
|
520
|
+
case 'walletPositions': {
|
|
521
|
+
// Single-wallet positions (open + historical).
|
|
522
|
+
// Predexon path: /v1/pm/polymarket/wallet/positions/{wallet} — Tier 2 ($0.005).
|
|
523
|
+
if (!wallets || !wallets.trim()) {
|
|
524
|
+
return {
|
|
525
|
+
output: 'Error: `wallets` is required for walletPositions (single Polymarket wallet address)',
|
|
526
|
+
isError: true,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const parsedWallets = parseWalletsInput(wallets);
|
|
530
|
+
if (parsedWallets.length !== 1) {
|
|
531
|
+
return {
|
|
532
|
+
output: 'Error: walletPositions accepts exactly one wallet address. For multiple wallets, call walletPositions once per address in parallel.',
|
|
533
|
+
isError: true,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const wallet = parsedWallets[0];
|
|
537
|
+
const raw = await getWithPayment(`/v1/pm/polymarket/wallet/positions/${encodeURIComponent(wallet)}`, { limit: cappedLimit }, ctx);
|
|
538
|
+
const positions = unwrapList(raw);
|
|
539
|
+
if (positions.length === 0) {
|
|
540
|
+
return { output: `No positions returned for ${wallet}` };
|
|
541
|
+
}
|
|
542
|
+
const w = wallet.length > 12 ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}` : wallet;
|
|
543
|
+
const lines = [
|
|
544
|
+
`## Polymarket positions — \`${w}\` — ${positions.length} position${positions.length === 1 ? '' : 's'}`,
|
|
545
|
+
'',
|
|
546
|
+
];
|
|
547
|
+
// Predexon returns each position as a nested record:
|
|
548
|
+
// { market: {title, side_label, ...},
|
|
549
|
+
// position: {shares, avg_entry_price, total_cost_usd, ...},
|
|
550
|
+
// current: {price, value_usd},
|
|
551
|
+
// pnl: {unrealized_usd, unrealized_pct, realized_usd} }
|
|
552
|
+
// Verified 2026-05-06 via FRANKLIN_PM_DEBUG=1 dump. Walk the four
|
|
553
|
+
// sub-objects rather than assuming flat fields. Keep flat-field
|
|
554
|
+
// fallbacks too in case the response shape changes or the user's
|
|
555
|
+
// gateway version returns a flatter format.
|
|
556
|
+
positions.slice(0, cappedLimit).forEach((p, i) => {
|
|
557
|
+
const market = (p.market && typeof p.market === 'object' ? p.market : {});
|
|
558
|
+
const position = (p.position && typeof p.position === 'object' ? p.position : {});
|
|
559
|
+
const current = (p.current && typeof p.current === 'object' ? p.current : {});
|
|
560
|
+
const pnlObj = (p.pnl && typeof p.pnl === 'object' ? p.pnl : {});
|
|
561
|
+
const title = pickString(market.title, market.question, p.title, p.question, market.market_slug, p.market_slug) ?? 'untitled';
|
|
562
|
+
const outcome = pickString(market.side_label, market.side, p.outcome, p.side);
|
|
563
|
+
const shares = position.shares ?? position.total_shares_bought ?? p.size ?? p.shares;
|
|
564
|
+
const avgPrice = position.avg_entry_price ?? p.avg_price ?? p.avgPrice;
|
|
565
|
+
const currentValue = current.value_usd ?? p.current_value ?? p.currentValue ?? p.value;
|
|
566
|
+
const pnl = pnlObj.unrealized_usd ?? pnlObj.realized_usd ?? p.cashPnl ?? p.pnl;
|
|
567
|
+
const pnlPct = pnlObj.unrealized_pct ?? pnlObj.realized_pct ?? p.percentPnl ?? p.percent_pnl;
|
|
568
|
+
const parts = [];
|
|
569
|
+
if (outcome)
|
|
570
|
+
parts.push(outcome);
|
|
571
|
+
if (shares != null)
|
|
572
|
+
parts.push(`${formatQuantity(shares)} shares`);
|
|
573
|
+
if (avgPrice != null)
|
|
574
|
+
parts.push(`avg ${formatPct(avgPrice)}`);
|
|
575
|
+
if (currentValue != null)
|
|
576
|
+
parts.push(`now ${formatUsd(currentValue)}`);
|
|
577
|
+
if (pnl != null) {
|
|
578
|
+
const pctStr = pnlPct != null ? ` (${formatPct(pnlPct, 1)})` : '';
|
|
579
|
+
parts.push(`P&L ${formatUsd(pnl)}${pctStr}`);
|
|
580
|
+
}
|
|
581
|
+
lines.push(`${i + 1}. **${title}** — ${parts.join(' · ')}`);
|
|
582
|
+
});
|
|
583
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
584
|
+
return { output: lines.join('\n') };
|
|
585
|
+
}
|
|
586
|
+
case 'smartActivity': {
|
|
587
|
+
// "Discover markets where high-performing wallets are active right now."
|
|
588
|
+
// Complements `smartMoney`: this discovers interesting markets across
|
|
589
|
+
// the venue; smartMoney drills into one condition_id.
|
|
590
|
+
const raw = await getWithPayment('/v1/pm/polymarket/markets/smart-activity', {
|
|
591
|
+
limit: cappedLimit,
|
|
592
|
+
search,
|
|
593
|
+
}, ctx);
|
|
594
|
+
const rows = unwrapList(raw);
|
|
595
|
+
if (rows.length === 0) {
|
|
596
|
+
return { output: 'No smart-money activity recorded right now.' };
|
|
597
|
+
}
|
|
598
|
+
const lines = [
|
|
599
|
+
`## Smart-money activity — ${rows.length} market${rows.length === 1 ? '' : 's'}`,
|
|
600
|
+
'_Markets where high-P&L Polymarket wallets are positioning right now._',
|
|
601
|
+
'',
|
|
602
|
+
];
|
|
603
|
+
rows.forEach((r, i) => {
|
|
604
|
+
const title = pickString(r.question, r.title, r.market, r.event, r.market_slug, r.slug) ?? 'untitled';
|
|
605
|
+
const cid = pickString(r.condition_id, r.id);
|
|
606
|
+
const cidTag = cid ? ` · \`${String(cid).slice(0, 14)}…\`` : '';
|
|
607
|
+
const smartCount = r.smart_wallets_count ?? r.wallet_count;
|
|
608
|
+
const netFlow = r.net_size ?? r.net_yes_size;
|
|
609
|
+
const stats = [];
|
|
610
|
+
if (smartCount != null)
|
|
611
|
+
stats.push(`${smartCount} smart wallet${smartCount === 1 ? '' : 's'}`);
|
|
612
|
+
if (netFlow != null)
|
|
613
|
+
stats.push(`net ${formatUsd(netFlow)}`);
|
|
614
|
+
lines.push(`${i + 1}. **${title}**${cidTag}` + (stats.length > 0 ? `\n ${stats.join(' · ')}` : ''));
|
|
615
|
+
});
|
|
616
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
617
|
+
return { output: lines.join('\n') };
|
|
618
|
+
}
|
|
619
|
+
case 'smartMoney': {
|
|
620
|
+
if (!conditionId) {
|
|
621
|
+
return {
|
|
622
|
+
output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket or smartActivity call)',
|
|
623
|
+
isError: true,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
// Per-market drill-down. Official live registry:
|
|
627
|
+
// /api/v1/pm/polymarket/market/:condition_id/smart-money
|
|
628
|
+
const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
|
|
629
|
+
const data = await getWithPayment(path, {}, ctx);
|
|
630
|
+
const buyers = (data.buyers ?? []).slice(0, 5);
|
|
631
|
+
const sellers = (data.sellers ?? []).slice(0, 5);
|
|
632
|
+
const lines = [
|
|
633
|
+
`## Smart money — \`${conditionId.slice(0, 14)}…\``,
|
|
634
|
+
];
|
|
635
|
+
if (data.net_yes_size != null || data.net_no_size != null) {
|
|
636
|
+
lines.push(`**Net flow:** YES ${formatUsd(data.net_yes_size)} / NO ${formatUsd(data.net_no_size)}`);
|
|
637
|
+
}
|
|
638
|
+
if (buyers.length > 0) {
|
|
639
|
+
lines.push('', '**Top buyers**');
|
|
640
|
+
buyers.forEach((b, i) => {
|
|
641
|
+
const w = b.wallet ? `${b.wallet.slice(0, 8)}…${b.wallet.slice(-4)}` : 'unknown';
|
|
642
|
+
lines.push(`${i + 1}. ${w} — ${formatUsd(b.size)} on ${b.outcome ?? 'unknown side'}`);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (sellers.length > 0) {
|
|
646
|
+
lines.push('', '**Top sellers**');
|
|
647
|
+
sellers.forEach((s, i) => {
|
|
648
|
+
const w = s.wallet ? `${s.wallet.slice(0, 8)}…${s.wallet.slice(-4)}` : 'unknown';
|
|
649
|
+
lines.push(`${i + 1}. ${w} — ${formatUsd(s.size)} on ${s.outcome ?? 'unknown side'}`);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (buyers.length === 0 && sellers.length === 0) {
|
|
653
|
+
lines.push('No smart-money flow recorded for this market yet.');
|
|
654
|
+
}
|
|
655
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
656
|
+
return { output: lines.join('\n') };
|
|
657
|
+
}
|
|
658
|
+
case 'searchPolymarket': {
|
|
659
|
+
const raw = await getWithPayment('/v1/pm/polymarket/markets', {
|
|
660
|
+
search,
|
|
661
|
+
status: status ?? 'active',
|
|
662
|
+
sort: sort ?? 'volume',
|
|
663
|
+
limit: cappedLimit,
|
|
664
|
+
}, ctx);
|
|
665
|
+
const markets = unwrapList(raw);
|
|
666
|
+
if (markets.length === 0) {
|
|
667
|
+
return { output: 'No Polymarket markets matched the filters.' };
|
|
668
|
+
}
|
|
669
|
+
const lines = [
|
|
670
|
+
`## Polymarket — ${markets.length} market${markets.length === 1 ? '' : 's'}` +
|
|
671
|
+
(search ? ` · search="${search}"` : '') +
|
|
672
|
+
(status ? ` · status=${status}` : '') +
|
|
673
|
+
` · sort=${sort ?? 'volume'}`,
|
|
674
|
+
'',
|
|
675
|
+
];
|
|
676
|
+
markets.forEach((m, i) => {
|
|
677
|
+
const yesPx = m.outcomes && m.outcome_prices && m.outcomes.length === m.outcome_prices.length
|
|
678
|
+
? m.outcomes.map((o, j) => `${o}=${formatPct(m.outcome_prices[j])}`).join(' / ')
|
|
679
|
+
: 'n/a';
|
|
680
|
+
const cid = m.condition_id ? ` · condition_id=\`${m.condition_id.slice(0, 14)}…\`` : '';
|
|
681
|
+
lines.push(`${i + 1}. **${m.question || m.market_slug || 'untitled'}**${cid}\n` +
|
|
682
|
+
` prices: ${yesPx} · vol: ${formatUsd(m.volume)} · liq: ${formatUsd(m.liquidity)}` +
|
|
683
|
+
(m.end_date ? ` · ends ${m.end_date.slice(0, 10)}` : ''));
|
|
684
|
+
});
|
|
685
|
+
lines.push('', `_$0.001 paid via x402._`);
|
|
686
|
+
return { output: lines.join('\n') };
|
|
687
|
+
}
|
|
688
|
+
case 'searchKalshi': {
|
|
689
|
+
const raw = await getWithPayment('/v1/pm/kalshi/markets', {
|
|
690
|
+
search,
|
|
691
|
+
status: status ?? 'open',
|
|
692
|
+
sort: sort ?? 'volume',
|
|
693
|
+
limit: cappedLimit,
|
|
694
|
+
}, ctx);
|
|
695
|
+
const markets = unwrapList(raw);
|
|
696
|
+
if (markets.length === 0) {
|
|
697
|
+
return { output: 'No Kalshi markets matched the filters.' };
|
|
698
|
+
}
|
|
699
|
+
const lines = [
|
|
700
|
+
`## Kalshi — ${markets.length} market${markets.length === 1 ? '' : 's'}` +
|
|
701
|
+
(search ? ` · search="${search}"` : '') +
|
|
702
|
+
` · status=${status ?? 'open'} · sort=${sort ?? 'volume'}`,
|
|
703
|
+
'',
|
|
704
|
+
];
|
|
705
|
+
markets.forEach((m, i) => {
|
|
706
|
+
// Kalshi quotes prices in cents (0–100). Surface them as a tight
|
|
707
|
+
// bid/ask so the agent can read implied probability at a glance.
|
|
708
|
+
const bid = m.yes_bid != null ? `${m.yes_bid}¢` : 'n/a';
|
|
709
|
+
const ask = m.yes_ask != null ? `${m.yes_ask}¢` : 'n/a';
|
|
710
|
+
const ticker = m.ticker ? ` · ticker=\`${m.ticker}\`` : '';
|
|
711
|
+
lines.push(`${i + 1}. **${m.title || m.ticker || 'untitled'}**${ticker}\n` +
|
|
712
|
+
` yes ${bid}/${ask} · vol: ${m.volume?.toLocaleString() ?? 'n/a'} · OI: ${m.open_interest?.toLocaleString() ?? 'n/a'}` +
|
|
713
|
+
(m.close_time ? ` · closes ${m.close_time.slice(0, 10)}` : ''));
|
|
714
|
+
});
|
|
715
|
+
lines.push('', `_$0.001 paid via x402._`);
|
|
716
|
+
return { output: lines.join('\n') };
|
|
717
|
+
}
|
|
718
|
+
case 'crossPlatform': {
|
|
719
|
+
const raw = await getWithPayment('/v1/pm/matching-markets/pairs', {
|
|
720
|
+
limit: cappedLimit,
|
|
721
|
+
}, ctx);
|
|
722
|
+
const pairs = unwrapList(raw);
|
|
723
|
+
if (pairs.length === 0) {
|
|
724
|
+
return { output: 'No matched market pairs available right now.' };
|
|
725
|
+
}
|
|
726
|
+
const lines = [
|
|
727
|
+
`## Cross-platform matched pairs — ${pairs.length}`,
|
|
728
|
+
'_Polymarket ↔ Kalshi equivalent markets. Use these to compare implied probabilities across venues._',
|
|
729
|
+
'',
|
|
730
|
+
];
|
|
731
|
+
pairs.forEach((p, i) => {
|
|
732
|
+
const sim = p.similarity != null ? ` · similarity ${formatPct(p.similarity, 0)}` : '';
|
|
733
|
+
lines.push(`${i + 1}. **Polymarket:** ${p.polymarket_question || '(untitled)'}\n` +
|
|
734
|
+
` **Kalshi:** ${p.kalshi_title || '(untitled)'}` +
|
|
735
|
+
(p.kalshi_ticker ? ` · ticker=\`${p.kalshi_ticker}\`` : '') +
|
|
736
|
+
sim);
|
|
737
|
+
});
|
|
738
|
+
lines.push('', `_$0.005 paid via x402._`);
|
|
739
|
+
return { output: lines.join('\n') };
|
|
740
|
+
}
|
|
741
|
+
default:
|
|
742
|
+
return {
|
|
743
|
+
output: `Error: unknown action "${action}". Use: searchAll, searchPolymarket, searchKalshi, crossPlatform, leaderboard, walletProfile, walletPnl, walletPositions, smartActivity, smartMoney`,
|
|
744
|
+
isError: true,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
return { output: `Error: ${err.message}`, isError: true };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
export const predictionMarketCapability = {
|
|
753
|
+
spec: {
|
|
754
|
+
name: 'PredictionMarket',
|
|
755
|
+
description: 'Real prediction market data via the BlockRun gateway (powered by Predexon). Use for any "what are the odds of X" / "Polymarket on Y" / "is there a market on Z" / "follow this trader" question. ' +
|
|
756
|
+
'Actions: ' +
|
|
757
|
+
'`searchAll` (search markets across Polymarket+Kalshi+Limitless+Opinion+Predict.Fun in one call — $0.005), ' +
|
|
758
|
+
'`searchPolymarket` (Polymarket only, supports sort+status — $0.001), ' +
|
|
759
|
+
'`searchKalshi` (Kalshi only, supports sort+status — $0.001), ' +
|
|
760
|
+
'`crossPlatform` (matched market pairs across Polymarket+Kalshi for arbitrage / consensus — $0.005), ' +
|
|
761
|
+
'`leaderboard` (global top wallets by P&L on Polymarket — $0.001), ' +
|
|
762
|
+
'`walletProfile` (full Polymarket wallet profile — labels, scores, stats. Single address → /wallet/{addr}; comma-list → batch /wallets/profiles — $0.005), ' +
|
|
763
|
+
'`walletPnl` (single Polymarket wallet P&L summary + time series — $0.005), ' +
|
|
764
|
+
'`walletPositions` (single Polymarket wallet positions — open + historical with P&L per position — $0.005), ' +
|
|
765
|
+
'`smartActivity` (markets where high-P&L wallets are positioning right now — $0.005), ' +
|
|
766
|
+
'`smartMoney` (smart-money positioning on one Polymarket condition_id — $0.005). ' +
|
|
767
|
+
'Default routing: ' +
|
|
768
|
+
'"is there a market on X anywhere" → searchAll. ' +
|
|
769
|
+
'"top wallets / who is profitable / who should I follow on Polymarket" → leaderboard. ' +
|
|
770
|
+
'"analyze this wallet / can I copy this trader / 复制交易 / show me their P&L AND positions" → run walletProfile + walletPnl + walletPositions IN PARALLEL with the same address — three $0.005 calls give the full picture for $0.015. Do not Bash-curl Polymarket directly; the agent has paid tools for this. ' +
|
|
771
|
+
'"what are smart traders betting on right now" → smartActivity. ' +
|
|
772
|
+
'"show smart money on this specific Polymarket market" → smartMoney with conditionId. ' +
|
|
773
|
+
'"should I bet on X" → run searchPolymarket + searchKalshi in parallel and compare implied probabilities — divergence is the signal.',
|
|
774
|
+
input_schema: {
|
|
775
|
+
type: 'object',
|
|
776
|
+
properties: {
|
|
777
|
+
action: {
|
|
778
|
+
type: 'string',
|
|
779
|
+
enum: [
|
|
780
|
+
'searchAll',
|
|
781
|
+
'searchPolymarket',
|
|
782
|
+
'searchKalshi',
|
|
783
|
+
'crossPlatform',
|
|
784
|
+
'leaderboard',
|
|
785
|
+
'walletProfile',
|
|
786
|
+
'walletPnl',
|
|
787
|
+
'walletPositions',
|
|
788
|
+
'smartActivity',
|
|
789
|
+
'smartMoney',
|
|
790
|
+
],
|
|
791
|
+
description: 'Which prediction-market query to run. See tool description for cost per action.',
|
|
792
|
+
},
|
|
793
|
+
search: {
|
|
794
|
+
type: 'string',
|
|
795
|
+
description: 'Search query. Used by searchAll / searchPolymarket / searchKalshi / smartActivity. Optional for crossPlatform/leaderboard/walletProfile/walletPnl/walletPositions/smartMoney.',
|
|
796
|
+
},
|
|
797
|
+
status: {
|
|
798
|
+
type: 'string',
|
|
799
|
+
description: 'Polymarket: active | closed | archived (default active). Kalshi: open | closed (default open). Forwarded to searchAll where supported.',
|
|
800
|
+
},
|
|
801
|
+
sort: {
|
|
802
|
+
type: 'string',
|
|
803
|
+
description: 'Polymarket: volume | liquidity | created (default volume). Kalshi: volume | open_interest | price_desc | price_asc | close_time (default volume). leaderboard: pnl | volume | win_rate (gateway-defined).',
|
|
804
|
+
},
|
|
805
|
+
limit: {
|
|
806
|
+
type: 'number',
|
|
807
|
+
description: `Max results (default ${DEFAULT_LIMIT}, hard cap ${MAX_LIMIT}).`,
|
|
808
|
+
},
|
|
809
|
+
wallets: {
|
|
810
|
+
type: 'string',
|
|
811
|
+
description: 'For walletProfile: a single Polymarket wallet address, or a comma-separated list of addresses for batch lookup.',
|
|
812
|
+
},
|
|
813
|
+
conditionId: {
|
|
814
|
+
type: 'string',
|
|
815
|
+
description: 'For smartMoney: Polymarket condition_id from searchPolymarket or smartActivity.',
|
|
816
|
+
},
|
|
817
|
+
granularity: {
|
|
818
|
+
type: 'string',
|
|
819
|
+
enum: ['day', 'week', 'month', 'year', 'all'],
|
|
820
|
+
description: 'For walletPnl: time bucket for the P&L series. Default day.',
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
required: ['action'],
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
execute,
|
|
827
|
+
concurrent: true,
|
|
828
|
+
};
|
package/dist/tools/prediction.js
CHANGED
|
@@ -102,7 +102,7 @@ async function getWithPayment(path, query, ctx) {
|
|
|
102
102
|
const errText = await response.text().catch(() => '');
|
|
103
103
|
// Surface failed paid calls in the Markets-tab health summary.
|
|
104
104
|
recordFetch({ provider: 'blockrun', endpoint: path, ok: false, latencyMs: Date.now() - startedAt });
|
|
105
|
-
throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0,
|
|
105
|
+
throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 600)}`);
|
|
106
106
|
}
|
|
107
107
|
recordFetch({
|
|
108
108
|
provider: 'blockrun',
|
|
@@ -229,8 +229,44 @@ function parseWalletsInput(value) {
|
|
|
229
229
|
.map(w => w.trim())
|
|
230
230
|
.filter(Boolean);
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Pick the first usable string from a list of candidate values.
|
|
234
|
+
*
|
|
235
|
+
* Predexon responses sometimes wrap titles/labels inside nested objects
|
|
236
|
+
* (e.g. `position.market = { slug, question, title }` instead of a flat
|
|
237
|
+
* `position.title`). Pre-3.15.75 the formatter `as string` cast these
|
|
238
|
+
* objects and ended up rendering `[object Object]` for every position
|
|
239
|
+
* row — verified 2026-05-06 in a real session.
|
|
240
|
+
*
|
|
241
|
+
* Strategy:
|
|
242
|
+
* - string → return as-is (after trim)
|
|
243
|
+
* - object → walk a small set of common name-bearing keys
|
|
244
|
+
* (title, question, slug, name, label, market_slug) and return the
|
|
245
|
+
* first one that yields a string
|
|
246
|
+
* - anything else (number / array / null) → skip
|
|
247
|
+
* - all candidates exhausted → undefined
|
|
248
|
+
*/
|
|
249
|
+
function pickString(...candidates) {
|
|
250
|
+
const NAME_KEYS = ['title', 'question', 'slug', 'name', 'label', 'market_slug', 'event_title'];
|
|
251
|
+
for (const c of candidates) {
|
|
252
|
+
if (typeof c === 'string') {
|
|
253
|
+
const trimmed = c.trim();
|
|
254
|
+
if (trimmed)
|
|
255
|
+
return trimmed;
|
|
256
|
+
}
|
|
257
|
+
else if (c && typeof c === 'object' && !Array.isArray(c)) {
|
|
258
|
+
const obj = c;
|
|
259
|
+
for (const k of NAME_KEYS) {
|
|
260
|
+
const v = obj[k];
|
|
261
|
+
if (typeof v === 'string' && v.trim())
|
|
262
|
+
return v.trim();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
232
268
|
async function execute(input, ctx) {
|
|
233
|
-
const { action, search, status, sort, limit, conditionId, wallets } = input;
|
|
269
|
+
const { action, search, status, sort, limit, conditionId, wallets, granularity } = input;
|
|
234
270
|
const cappedLimit = Math.min(Math.max(1, limit ?? DEFAULT_LIMIT), MAX_LIMIT);
|
|
235
271
|
if (!action) {
|
|
236
272
|
return {
|
|
@@ -244,8 +280,12 @@ async function execute(input, ctx) {
|
|
|
244
280
|
// One $0.005 call across 5 platforms — Polymarket, Kalshi, Limitless,
|
|
245
281
|
// Opinion, Predict.Fun. The right entry point for "is there a market
|
|
246
282
|
// on X anywhere?" — beats firing per-platform searches in parallel.
|
|
283
|
+
// Predexon expects `q` for the search term — verified 2026-05-06 from
|
|
284
|
+
// a live 422: {"detail":[{"type":"missing","loc":["query","q"]}]}.
|
|
285
|
+
// Public input field stays `search` for ergonomic consistency with
|
|
286
|
+
// searchPolymarket / searchKalshi; rename on the wire.
|
|
247
287
|
const raw = await getWithPayment('/v1/pm/markets/search', {
|
|
248
|
-
search,
|
|
288
|
+
q: search,
|
|
249
289
|
status,
|
|
250
290
|
sort,
|
|
251
291
|
limit: cappedLimit,
|
|
@@ -271,8 +311,8 @@ async function execute(input, ctx) {
|
|
|
271
311
|
const shown = list.slice(0, Math.min(5, remaining));
|
|
272
312
|
lines.push(`### ${p}`);
|
|
273
313
|
shown.forEach((m, i) => {
|
|
274
|
-
const title = (m.title
|
|
275
|
-
const id = (m.condition_id
|
|
314
|
+
const title = pickString(m.title, m.question, m.market, m.event, m.market_slug, m.slug, m.ticker) ?? 'untitled';
|
|
315
|
+
const id = pickString(m.condition_id, m.ticker, m.id);
|
|
276
316
|
const idTag = id ? ` · \`${String(id).slice(0, 18)}…\`` : '';
|
|
277
317
|
const vol = m.volume != null ? ` · vol ${formatUsd(m.volume)}` : '';
|
|
278
318
|
lines.push(`${i + 1}. ${title}${idTag}${vol}`);
|
|
@@ -287,8 +327,8 @@ async function execute(input, ctx) {
|
|
|
287
327
|
return { output: 'No markets matched across any platform.' };
|
|
288
328
|
}
|
|
289
329
|
flat.slice(0, cappedLimit).forEach((m, i) => {
|
|
290
|
-
const title = (m.title
|
|
291
|
-
const platform = (m.platform
|
|
330
|
+
const title = pickString(m.title, m.question, m.market, m.event, m.market_slug, m.slug, m.ticker) ?? 'untitled';
|
|
331
|
+
const platform = pickString(m.platform, m.source) ?? 'unknown';
|
|
292
332
|
lines.push(`${i + 1}. **[${platform}]** ${title}`);
|
|
293
333
|
});
|
|
294
334
|
}
|
|
@@ -323,14 +363,14 @@ async function execute(input, ctx) {
|
|
|
323
363
|
'',
|
|
324
364
|
];
|
|
325
365
|
rows.forEach((r, i) => {
|
|
326
|
-
const wallet = (r.wallet
|
|
366
|
+
const wallet = pickString(r.wallet, r.address, r.proxy_wallet, r.proxyWallet) ?? 'unknown';
|
|
327
367
|
const w = wallet.length > 12
|
|
328
368
|
? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
|
|
329
369
|
: wallet;
|
|
330
370
|
const pnl = r.pnl ?? r.realized_pnl ?? r.total_pnl;
|
|
331
371
|
const volume = r.volume ?? r.total_volume;
|
|
332
372
|
const winRate = r.win_rate ?? r.winRate;
|
|
333
|
-
const name = (r.name
|
|
373
|
+
const name = pickString(r.name, r.handle, r.username);
|
|
334
374
|
const handle = name ? ` (${name})` : '';
|
|
335
375
|
const parts = [];
|
|
336
376
|
if (pnl != null)
|
|
@@ -385,31 +425,76 @@ async function execute(input, ctx) {
|
|
|
385
425
|
`## Polymarket wallet profile${profiles.length === 1 ? '' : 's'} — ${profiles.length}`,
|
|
386
426
|
'',
|
|
387
427
|
];
|
|
428
|
+
// Real Predexon shape (single-wallet, verified 2026-05-06):
|
|
429
|
+
// { user, metrics: { one_day, seven_day, thirty_day, all_time } }
|
|
430
|
+
// Each metric block has: realized_pnl, total_pnl, volume, roi,
|
|
431
|
+
// trades, wins, losses, win_rate, profit_factor, positions_closed,
|
|
432
|
+
// plus all_time-only: avg_buy_price, avg_sell_price,
|
|
433
|
+
// avg_hold_time_seconds, wallet_age_days, total_positions,
|
|
434
|
+
// active_positions, max_win_streak, max_loss_streak,
|
|
435
|
+
// best_position_realized_pnl, worst_position_realized_pnl
|
|
436
|
+
// Batch endpoint (multiple wallets) likely returns an array of these
|
|
437
|
+
// same blocks. We surface all_time as the headline stats and a
|
|
438
|
+
// compact one_day / seven_day / thirty_day delta line so the agent
|
|
439
|
+
// can see momentum.
|
|
388
440
|
profiles.forEach((p, i) => {
|
|
389
|
-
const
|
|
441
|
+
const metrics = (p.metrics && typeof p.metrics === 'object' ? p.metrics : {});
|
|
442
|
+
const allTime = (metrics.all_time && typeof metrics.all_time === 'object' ? metrics.all_time : {});
|
|
443
|
+
const oneDay = (metrics.one_day && typeof metrics.one_day === 'object' ? metrics.one_day : {});
|
|
444
|
+
const sevenDay = (metrics.seven_day && typeof metrics.seven_day === 'object' ? metrics.seven_day : {});
|
|
445
|
+
const thirtyDay = (metrics.thirty_day && typeof metrics.thirty_day === 'object' ? metrics.thirty_day : {});
|
|
446
|
+
const wallet = pickString(p.user, p.wallet, p.address, p.proxy_wallet, p.proxyWallet) ?? 'unknown';
|
|
390
447
|
const w = wallet.length > 12
|
|
391
448
|
? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
|
|
392
449
|
: wallet;
|
|
393
|
-
const name = (p.name
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
450
|
+
const name = pickString(p.name, p.handle, p.username);
|
|
451
|
+
// Headline stats from all_time (fall back to flat fields if the
|
|
452
|
+
// shape is ever flatter than today's nested format).
|
|
453
|
+
const pnl = allTime.total_pnl ?? allTime.realized_pnl ?? p.pnl ?? p.realized_pnl;
|
|
454
|
+
const realized = allTime.realized_pnl ?? p.realized_pnl;
|
|
455
|
+
const volume = allTime.volume ?? p.volume ?? p.total_volume;
|
|
456
|
+
const trades = allTime.trades ?? p.trades;
|
|
457
|
+
const winRate = allTime.win_rate ?? p.win_rate ?? p.winRate;
|
|
458
|
+
const roi = allTime.roi ?? p.roi;
|
|
459
|
+
const activePositions = allTime.active_positions ?? p.active_positions ?? p.positions_count;
|
|
460
|
+
const ageDays = allTime.wallet_age_days ?? p.wallet_age_days;
|
|
399
461
|
lines.push(`${i + 1}. \`${w}\`` + (name ? ` (${name})` : ''));
|
|
400
462
|
const stats = [];
|
|
401
463
|
if (pnl != null)
|
|
402
|
-
stats.push(`P&L ${formatUsd(pnl)}`);
|
|
403
|
-
if (
|
|
404
|
-
stats.push(`
|
|
464
|
+
stats.push(`total P&L ${formatUsd(pnl)}`);
|
|
465
|
+
if (realized != null && realized !== pnl)
|
|
466
|
+
stats.push(`realized ${formatUsd(realized)}`);
|
|
405
467
|
if (volume != null)
|
|
406
468
|
stats.push(`vol ${formatUsd(volume)}`);
|
|
407
|
-
if (
|
|
408
|
-
stats.push(
|
|
469
|
+
if (roi != null)
|
|
470
|
+
stats.push(`ROI ${formatPct(roi, 1)}`);
|
|
409
471
|
if (winRate != null)
|
|
410
472
|
stats.push(`win ${formatPct(winRate, 0)}`);
|
|
473
|
+
if (trades != null)
|
|
474
|
+
stats.push(`${trades} trades`);
|
|
475
|
+
if (activePositions != null)
|
|
476
|
+
stats.push(`${activePositions} open`);
|
|
477
|
+
if (ageDays != null)
|
|
478
|
+
stats.push(`${ageDays}d age`);
|
|
411
479
|
if (stats.length > 0)
|
|
412
480
|
lines.push(` ${stats.join(' · ')}`);
|
|
481
|
+
// Recent-window deltas help the agent judge momentum without a
|
|
482
|
+
// separate walletPnl call.
|
|
483
|
+
const recent = [];
|
|
484
|
+
for (const [label, block] of [['1d', oneDay], ['7d', sevenDay], ['30d', thirtyDay]]) {
|
|
485
|
+
const tp = block.total_pnl;
|
|
486
|
+
const tr = block.trades;
|
|
487
|
+
if (tp != null || tr != null) {
|
|
488
|
+
const parts = [];
|
|
489
|
+
if (tp != null)
|
|
490
|
+
parts.push(formatUsd(tp));
|
|
491
|
+
if (tr != null)
|
|
492
|
+
parts.push(`${tr} trades`);
|
|
493
|
+
recent.push(`${label}: ${parts.join(' / ')}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (recent.length > 0)
|
|
497
|
+
lines.push(` ${recent.join(' · ')}`);
|
|
413
498
|
});
|
|
414
499
|
lines.push('', `_$0.005 paid via x402._`);
|
|
415
500
|
return { output: lines.join('\n') };
|
|
@@ -431,43 +516,61 @@ async function execute(input, ctx) {
|
|
|
431
516
|
};
|
|
432
517
|
}
|
|
433
518
|
const wallet = parsedWallets[0];
|
|
434
|
-
|
|
519
|
+
// Predexon requires `granularity` from the enum {day, week, month,
|
|
520
|
+
// year, all} — verified 2026-05-06 in two live 422 turns. Default
|
|
521
|
+
// `day`; agent can override via input field for longer aggregations.
|
|
522
|
+
const raw = await getWithPayment(`/v1/pm/polymarket/wallet/pnl/${encodeURIComponent(wallet)}`, { granularity: granularity ?? 'day' }, ctx);
|
|
435
523
|
if (!raw || typeof raw !== 'object') {
|
|
436
524
|
return { output: `No P&L data returned for ${wallet}` };
|
|
437
525
|
}
|
|
526
|
+
// Real Predexon shape (verified 2026-05-06):
|
|
527
|
+
// { granularity, start_time, end_time, wallet_address,
|
|
528
|
+
// realized_pnl, unrealized_pnl, total_pnl,
|
|
529
|
+
// fees_paid, fees_refunded,
|
|
530
|
+
// pnl_over_time: [{timestamp, pnl_to_date}, ...] }
|
|
531
|
+
// start_time/end_time and timestamp are unix seconds.
|
|
438
532
|
const data = raw;
|
|
439
|
-
const realized = data.realized_pnl ?? data.realizedPnl
|
|
533
|
+
const realized = data.realized_pnl ?? data.realizedPnl;
|
|
440
534
|
const unrealized = data.unrealized_pnl ?? data.unrealizedPnl;
|
|
441
|
-
const total = data.
|
|
442
|
-
const
|
|
443
|
-
const winRate = data.win_rate ?? data.winRate;
|
|
535
|
+
const total = data.total_pnl ?? data.totalPnl;
|
|
536
|
+
const fees = data.fees_paid ?? data.feesPaid;
|
|
444
537
|
const w = wallet.length > 12 ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}` : wallet;
|
|
445
|
-
const
|
|
538
|
+
const gran = data.granularity ?? granularity ?? 'day';
|
|
539
|
+
const lines = [`## Polymarket wallet P&L — \`${w}\` · granularity ${gran}`, ''];
|
|
446
540
|
const summary = [];
|
|
541
|
+
if (total != null)
|
|
542
|
+
summary.push(`total ${formatUsd(total)}`);
|
|
447
543
|
if (realized != null)
|
|
448
544
|
summary.push(`realized ${formatUsd(realized)}`);
|
|
449
545
|
if (unrealized != null)
|
|
450
546
|
summary.push(`unrealized ${formatUsd(unrealized)}`);
|
|
451
|
-
if (
|
|
452
|
-
summary.push(`
|
|
453
|
-
if (volume != null)
|
|
454
|
-
summary.push(`vol ${formatUsd(volume)}`);
|
|
455
|
-
if (winRate != null)
|
|
456
|
-
summary.push(`win ${formatPct(winRate, 0)}`);
|
|
547
|
+
if (fees != null && Number(fees) > 0)
|
|
548
|
+
summary.push(`fees ${formatUsd(fees)}`);
|
|
457
549
|
if (summary.length > 0)
|
|
458
550
|
lines.push(summary.join(' · '));
|
|
459
|
-
//
|
|
460
|
-
|
|
551
|
+
// Time series: pnl_over_time uses unix seconds. Show last 7 non-zero
|
|
552
|
+
// checkpoints so the agent sees momentum without paginating through
|
|
553
|
+
// hundreds of zero-pnl warmup days.
|
|
554
|
+
const series = (data.pnl_over_time ?? data.pnlOverTime ?? data.series);
|
|
461
555
|
if (Array.isArray(series) && series.length > 0) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const v = (pt.pnl ?? pt.value ?? pt.cumulative_pnl);
|
|
466
|
-
if (t != null && v != null) {
|
|
467
|
-
const tStr = typeof t === 'number' ? new Date(t).toISOString().slice(0, 10) : String(t).slice(0, 10);
|
|
468
|
-
lines.push(`- ${tStr} · ${formatUsd(v)}`);
|
|
469
|
-
}
|
|
556
|
+
const meaningful = series.filter(pt => {
|
|
557
|
+
const v = (pt.pnl_to_date ?? pt.pnlToDate ?? pt.pnl ?? pt.value);
|
|
558
|
+
return typeof v === 'number' && v !== 0;
|
|
470
559
|
});
|
|
560
|
+
const sample = (meaningful.length > 0 ? meaningful : series).slice(-7);
|
|
561
|
+
if (sample.length > 0) {
|
|
562
|
+
lines.push('', `**Recent points** (latest ${sample.length} of ${series.length}):`);
|
|
563
|
+
sample.forEach(pt => {
|
|
564
|
+
const t = (pt.timestamp ?? pt.ts ?? pt.date);
|
|
565
|
+
const v = (pt.pnl_to_date ?? pt.pnlToDate ?? pt.pnl ?? pt.value);
|
|
566
|
+
if (t != null && v != null) {
|
|
567
|
+
// Predexon timestamps are unix SECONDS, not millis.
|
|
568
|
+
const ms = typeof t === 'number' ? t * 1000 : Date.parse(String(t));
|
|
569
|
+
const tStr = Number.isFinite(ms) ? new Date(ms).toISOString().slice(0, 10) : String(t).slice(0, 10);
|
|
570
|
+
lines.push(`- ${tStr} · ${formatUsd(v)}`);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
471
574
|
}
|
|
472
575
|
lines.push('', `_$0.005 paid via x402._`);
|
|
473
576
|
return { output: lines.join('\n') };
|
|
@@ -499,19 +602,32 @@ async function execute(input, ctx) {
|
|
|
499
602
|
`## Polymarket positions — \`${w}\` — ${positions.length} position${positions.length === 1 ? '' : 's'}`,
|
|
500
603
|
'',
|
|
501
604
|
];
|
|
605
|
+
// Predexon returns each position as a nested record:
|
|
606
|
+
// { market: {title, side_label, ...},
|
|
607
|
+
// position: {shares, avg_entry_price, total_cost_usd, ...},
|
|
608
|
+
// current: {price, value_usd},
|
|
609
|
+
// pnl: {unrealized_usd, unrealized_pct, realized_usd} }
|
|
610
|
+
// Verified 2026-05-06 via FRANKLIN_PM_DEBUG=1 dump. Walk the four
|
|
611
|
+
// sub-objects rather than assuming flat fields. Keep flat-field
|
|
612
|
+
// fallbacks too in case the response shape changes or the user's
|
|
613
|
+
// gateway version returns a flatter format.
|
|
502
614
|
positions.slice(0, cappedLimit).forEach((p, i) => {
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const
|
|
615
|
+
const market = (p.market && typeof p.market === 'object' ? p.market : {});
|
|
616
|
+
const position = (p.position && typeof p.position === 'object' ? p.position : {});
|
|
617
|
+
const current = (p.current && typeof p.current === 'object' ? p.current : {});
|
|
618
|
+
const pnlObj = (p.pnl && typeof p.pnl === 'object' ? p.pnl : {});
|
|
619
|
+
const title = pickString(market.title, market.question, p.title, p.question, market.market_slug, p.market_slug) ?? 'untitled';
|
|
620
|
+
const outcome = pickString(market.side_label, market.side, p.outcome, p.side);
|
|
621
|
+
const shares = position.shares ?? position.total_shares_bought ?? p.size ?? p.shares;
|
|
622
|
+
const avgPrice = position.avg_entry_price ?? p.avg_price ?? p.avgPrice;
|
|
623
|
+
const currentValue = current.value_usd ?? p.current_value ?? p.currentValue ?? p.value;
|
|
624
|
+
const pnl = pnlObj.unrealized_usd ?? pnlObj.realized_usd ?? p.cashPnl ?? p.pnl;
|
|
625
|
+
const pnlPct = pnlObj.unrealized_pct ?? pnlObj.realized_pct ?? p.percentPnl ?? p.percent_pnl;
|
|
510
626
|
const parts = [];
|
|
511
627
|
if (outcome)
|
|
512
628
|
parts.push(outcome);
|
|
513
|
-
if (
|
|
514
|
-
parts.push(
|
|
629
|
+
if (shares != null)
|
|
630
|
+
parts.push(`${formatQuantity(shares)} shares`);
|
|
515
631
|
if (avgPrice != null)
|
|
516
632
|
parts.push(`avg ${formatPct(avgPrice)}`);
|
|
517
633
|
if (currentValue != null)
|
|
@@ -543,8 +659,8 @@ async function execute(input, ctx) {
|
|
|
543
659
|
'',
|
|
544
660
|
];
|
|
545
661
|
rows.forEach((r, i) => {
|
|
546
|
-
const title = (r.question
|
|
547
|
-
const cid = (r.condition_id
|
|
662
|
+
const title = pickString(r.question, r.title, r.market, r.event, r.market_slug, r.slug) ?? 'untitled';
|
|
663
|
+
const cid = pickString(r.condition_id, r.id);
|
|
548
664
|
const cidTag = cid ? ` · \`${String(cid).slice(0, 14)}…\`` : '';
|
|
549
665
|
const smartCount = r.smart_wallets_count ?? r.wallet_count;
|
|
550
666
|
const netFlow = r.net_size ?? r.net_yes_size;
|
|
@@ -756,6 +872,11 @@ export const predictionMarketCapability = {
|
|
|
756
872
|
type: 'string',
|
|
757
873
|
description: 'For smartMoney: Polymarket condition_id from searchPolymarket or smartActivity.',
|
|
758
874
|
},
|
|
875
|
+
granularity: {
|
|
876
|
+
type: 'string',
|
|
877
|
+
enum: ['day', 'week', 'month', 'year', 'all'],
|
|
878
|
+
description: 'For walletPnl: time bucket for the P&L series. Default day.',
|
|
879
|
+
},
|
|
759
880
|
},
|
|
760
881
|
required: ['action'],
|
|
761
882
|
},
|
package/package.json
CHANGED