@blockrun/franklin 3.15.69 → 3.15.71

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.
@@ -336,12 +336,16 @@ 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. Four actions:
340
- - \`searchPolymarket\` (\$0.001) and \`searchKalshi\` (\$0.001) search markets by keyword. Run them **in parallel** when the user wants the current odds; comparing implied probability across two venues is the high-value answer.
341
- - \`crossPlatform\` (\$0.005) pre-matched pairs of equivalent markets across Polymarket and Kalshi. Use when the user wants arbitrage candidates or wants to know "where does the consensus disagree".
342
- - \`smartMoney\` (\$0.005) top-wallet flow on a specific Polymarket \`condition_id\`. Get the \`condition_id\` from a prior \`searchPolymarket\` call.
343
-
344
- NEVER answer "what are the odds of X" from training-data memory these are live markets that move every minute. NEVER claim "Polymarket doesn't have a market on this" without running \`searchPolymarket\` first. If both Polymarket and Kalshi return zero markets, say so explicitly with the searches you tried, then offer to broaden the query.
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. Eight actions, route by intent:
340
+ - "is there a market on X anywhere?" / unknown which platform \`searchAll\` (\$0.005) single call across Polymarket+Kalshi+Limitless+Opinion+Predict.Fun.
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
+ - "where do Polymarket and Kalshi disagree / arbitrage" \`crossPlatform\` (\$0.005) returns pre-matched pairs.
343
+ - "who's profitable / top traders / who should I follow on Polymarket" → \`leaderboard\` (\$0.001) — global top wallets by P&L.
344
+ - "how is wallet 0xabc doing / show this trader's P&L / are they profitable" \`walletProfile\` (\$0.005) with \`wallets="<address>"\` (comma-separated for batch).
345
+ - "what are smart traders betting on right now / smart money flow across markets" → \`smartActivity\` (\$0.005) — markets where high-P&L wallets are positioning.
346
+ - "show smart money on this specific Polymarket market / this condition_id" → \`smartMoney\` (\$0.005) with \`conditionId="<condition_id>"\`.
347
+
348
+ NEVER answer "what are the odds of X" from training-data memory — these are live markets that move every minute. NEVER claim "no market on this" without running \`searchAll\` (or at least \`searchPolymarket\`) first. If a search returns zero, say so with the query you tried and offer to broaden.
345
349
 
346
350
  **Trading verdicts (TradingSignal).** When the user asks "how does $TICKER look" / "should I buy X" / "is BTC overbought":
347
351
  - Run **TradingSignal** with default lookback (90d). Lower values leave MACD undefined.
@@ -939,10 +939,20 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
939
939
  // never approached its 1M-token compaction threshold. Compact here
940
940
  // when the turn has accumulated lots of tool calls AND real spend,
941
941
  // even though the context window isn't close to full.
942
+ //
943
+ // Thresholds tightened in 3.15.71. Original 3.15.69 used
944
+ // (>30 calls AND >$0.05) — verified too loose against a real
945
+ // franklin-shorts edit session: 16 deepseek-v4-pro calls for
946
+ // $0.055 ended naturally before the trigger fired, even though
947
+ // by call #4 the per-call input was already 13K tokens (worth
948
+ // compacting). Lowering to (>15 AND >$0.03) catches sessions
949
+ // where input-replay tax has clearly started biting; the
950
+ // fire-once-per-turn flag still bounds the worst case at one
951
+ // extra summary call (~$0.005).
942
952
  if (!bloatCompactedThisTurn &&
943
953
  compactFailures < 3 &&
944
- turnToolCalls > 30 &&
945
- turnCostUsd > 0.05) {
954
+ turnToolCalls > 15 &&
955
+ turnCostUsd > 0.03) {
946
956
  try {
947
957
  const beforeTokens = estimateHistoryTokens(history);
948
958
  const { history: compacted, compacted: didCompact } = await forceCompact(history, config.model, client, config.debug);
@@ -650,7 +650,7 @@ a:hover { text-decoration:underline; }
650
650
  <div class="tab" id="tab-markets">
651
651
  <div class="content-header">
652
652
  <h2>Markets</h2>
653
- <p>How Franklin gets trading data — and what it costs.</p>
653
+ <p>How Franklin gets trading + prediction-market data — and what it costs.</p>
654
654
  </div>
655
655
 
656
656
  <div class="grid grid-4">
@@ -114,7 +114,18 @@ export function extractLastUserPrompt(history) {
114
114
  const text = flattenContent(msg.content);
115
115
  if (!text)
116
116
  continue;
117
- return text.replace(/\s+/g, ' ').trim();
117
+ const cleaned = text.replace(/\s+/g, ' ').trim();
118
+ // Anthropic's message format puts harness-injected context, guardrail
119
+ // warnings, and grounding-retry feedback under role:"user" too. Walking
120
+ // back blindly returns those synthetic strings instead of the real user
121
+ // intent — verified 2026-05-06 in the audit log: 403 entries showed
122
+ // "[FRANKLIN HARNESS PREFETCH] CRCL price..." and 18 showed
123
+ // "[GROUNDING CHECK FAILED] ..." instead of the user's actual question.
124
+ // Skip any message whose first non-whitespace block is a SCREAMING-CASE
125
+ // bracketed label and keep walking back to the real user turn.
126
+ if (/^\[[A-Z][A-Z _-]+\]/.test(cleaned))
127
+ continue;
128
+ return cleaned;
118
129
  }
119
130
  return undefined;
120
131
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
- * PredictionMarket — unified access to Polymarket / Kalshi / cross-platform
3
- * matching / smart-money endpoints via the BlockRun gateway. Each call
4
- * settles via x402 against the user's USDC wallet.
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.
5
6
  *
6
7
  * Powered server-side by Predexon; surfaced to the agent as a single
7
8
  * action-dispatched tool so the inventory stays small. Keep one cohesive
@@ -9,12 +10,19 @@
9
10
  * one-shot capabilities, otherwise weak models start hallucinating tool
10
11
  * names.
11
12
  *
13
+ * searchAll $0.005 search markets across Polymarket+Kalshi+
14
+ * Limitless+Opinion+Predict.Fun in one call
12
15
  * searchPolymarket $0.001 query Polymarket markets (event filter, sort)
13
16
  * searchKalshi $0.001 query Kalshi markets
14
17
  * crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
15
18
  * (the arbitrage / consensus signal)
19
+ * leaderboard $0.001 global Polymarket leaderboard — top wallets by P&L
20
+ * walletProfile $0.005 batch profile lookup for one or more Polymarket
21
+ * wallets — P&L, positions, identity
22
+ * smartActivity $0.005 discover markets where high-performing wallets
23
+ * are active right now
16
24
  * smartMoney $0.005 smart-money positioning on one Polymarket
17
- * condition_id (top wallet flow + side bias)
25
+ * condition_id (per-market drill-down)
18
26
  *
19
27
  * Output is filtered + truncated on the way back so a single call never
20
28
  * dumps 100 markets into the agent's context. Default 20 rows; agents that
@@ -1,7 +1,8 @@
1
1
  /**
2
- * PredictionMarket — unified access to Polymarket / Kalshi / cross-platform
3
- * matching / smart-money endpoints via the BlockRun gateway. Each call
4
- * settles via x402 against the user's USDC wallet.
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.
5
6
  *
6
7
  * Powered server-side by Predexon; surfaced to the agent as a single
7
8
  * action-dispatched tool so the inventory stays small. Keep one cohesive
@@ -9,12 +10,19 @@
9
10
  * one-shot capabilities, otherwise weak models start hallucinating tool
10
11
  * names.
11
12
  *
13
+ * searchAll $0.005 search markets across Polymarket+Kalshi+
14
+ * Limitless+Opinion+Predict.Fun in one call
12
15
  * searchPolymarket $0.001 query Polymarket markets (event filter, sort)
13
16
  * searchKalshi $0.001 query Kalshi markets
14
17
  * crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
15
18
  * (the arbitrage / consensus signal)
19
+ * leaderboard $0.001 global Polymarket leaderboard — top wallets by P&L
20
+ * walletProfile $0.005 batch profile lookup for one or more Polymarket
21
+ * wallets — P&L, positions, identity
22
+ * smartActivity $0.005 discover markets where high-performing wallets
23
+ * are active right now
16
24
  * smartMoney $0.005 smart-money positioning on one Polymarket
17
- * condition_id (top wallet flow + side bias)
25
+ * condition_id (per-market drill-down)
18
26
  *
19
27
  * Output is filtered + truncated on the way back so a single call never
20
28
  * dumps 100 markets into the agent's context. Default 20 rows; agents that
@@ -23,9 +31,31 @@
23
31
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
24
32
  import { loadChain, API_URLS, VERSION } from '../config.js';
25
33
  import { logger } from '../logger.js';
34
+ import { recordFetch } from '../trading/providers/telemetry.js';
26
35
  const TIMEOUT_MS = 30_000;
27
36
  const DEFAULT_LIMIT = 20;
28
37
  const MAX_LIMIT = 50;
38
+ // Per-action price table — mirrors the Predexon openapi.json. Used to feed
39
+ // the Markets-tab telemetry ring buffer so prediction-market spend appears
40
+ // in "Calls today / Spend today / Recent paid calls" alongside trading calls.
41
+ // If a path isn't here we don't record cost — we still record the fetch
42
+ // (success/latency) so panel health stays accurate.
43
+ const PATH_PRICES = [
44
+ { pattern: /\/v1\/pm\/markets\/search$/, usd: 0.005 },
45
+ { pattern: /\/v1\/pm\/matching-markets/, usd: 0.005 },
46
+ { pattern: /\/v1\/pm\/polymarket\/wallets\//, usd: 0.005 },
47
+ { pattern: /\/v1\/pm\/polymarket\/wallet\//, usd: 0.005 },
48
+ { pattern: /\/v1\/pm\/polymarket\/market\/[^/]+\/smart-money$/, usd: 0.005 },
49
+ { pattern: /\/v1\/pm\/polymarket\/markets\/smart-activity$/, usd: 0.005 },
50
+ { pattern: /\/v1\/pm\/.+/, usd: 0.001 },
51
+ ];
52
+ function priceForPath(path) {
53
+ for (const { pattern, usd } of PATH_PRICES) {
54
+ if (pattern.test(path))
55
+ return usd;
56
+ }
57
+ return 0;
58
+ }
29
59
  // ─── Shared GET-with-x402 flow ────────────────────────────────────────────
30
60
  async function getWithPayment(path, query, ctx) {
31
61
  const chain = loadChain();
@@ -46,6 +76,8 @@ async function getWithPayment(path, query, ctx) {
46
76
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
47
77
  const onAbort = () => controller.abort();
48
78
  ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
79
+ const startedAt = Date.now();
80
+ let costRecorded = 0;
49
81
  try {
50
82
  let response = await fetch(endpoint, { method: 'GET', signal: controller.signal, headers });
51
83
  if (response.status === 402) {
@@ -58,11 +90,23 @@ async function getWithPayment(path, query, ctx) {
58
90
  signal: controller.signal,
59
91
  headers: { ...headers, ...paymentHeaders },
60
92
  });
93
+ // Only record cost on the post-402 settlement; the initial 402
94
+ // response is free and counting it would double-charge the panel.
95
+ costRecorded = priceForPath(path);
61
96
  }
62
97
  if (!response.ok) {
63
98
  const errText = await response.text().catch(() => '');
99
+ // Surface failed paid calls in the Markets-tab health summary.
100
+ recordFetch({ provider: 'blockrun', endpoint: path, ok: false, latencyMs: Date.now() - startedAt });
64
101
  throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 200)}`);
65
102
  }
103
+ recordFetch({
104
+ provider: 'blockrun',
105
+ endpoint: path,
106
+ ok: true,
107
+ latencyMs: Date.now() - startedAt,
108
+ costUsd: costRecorded > 0 ? costRecorded : undefined,
109
+ });
66
110
  return (await response.json());
67
111
  }
68
112
  finally {
@@ -120,21 +164,33 @@ async function extractPaymentReq(response) {
120
164
  return header;
121
165
  }
122
166
  // ─── Formatting helpers ────────────────────────────────────────────────────
167
+ function asNumber(value) {
168
+ if (typeof value === 'number' && Number.isFinite(value))
169
+ return value;
170
+ if (typeof value === 'string' && value.trim() !== '') {
171
+ const n = Number(value);
172
+ if (Number.isFinite(n))
173
+ return n;
174
+ }
175
+ return null;
176
+ }
123
177
  function formatUsd(value) {
124
- if (value == null || !Number.isFinite(value))
178
+ const n = asNumber(value);
179
+ if (n == null)
125
180
  return 'n/a';
126
- if (value >= 1e9)
127
- return `$${(value / 1e9).toFixed(2)}B`;
128
- if (value >= 1e6)
129
- return `$${(value / 1e6).toFixed(2)}M`;
130
- if (value >= 1e3)
131
- return `$${(value / 1e3).toFixed(1)}K`;
132
- return `$${value.toFixed(2)}`;
181
+ if (n >= 1e9)
182
+ return `$${(n / 1e9).toFixed(2)}B`;
183
+ if (n >= 1e6)
184
+ return `$${(n / 1e6).toFixed(2)}M`;
185
+ if (n >= 1e3)
186
+ return `$${(n / 1e3).toFixed(1)}K`;
187
+ return `$${n.toFixed(2)}`;
133
188
  }
134
189
  function formatPct(value, digits = 1) {
135
- if (value == null || !Number.isFinite(value))
190
+ const n = asNumber(value);
191
+ if (n == null)
136
192
  return 'n/a';
137
- return `${(value * 100).toFixed(digits)}%`;
193
+ return `${(n * 100).toFixed(digits)}%`;
138
194
  }
139
195
  // API responses sometimes come wrapped as `{data: [...], pagination: ...}`,
140
196
  // other times as a bare array. Normalise to an array.
@@ -155,13 +211,242 @@ function unwrapList(raw) {
155
211
  return [];
156
212
  }
157
213
  async function execute(input, ctx) {
158
- const { action, search, status, sort, limit, conditionId } = input;
214
+ const { action, search, status, sort, limit, conditionId, wallets } = input;
159
215
  const cappedLimit = Math.min(Math.max(1, limit ?? DEFAULT_LIMIT), MAX_LIMIT);
160
216
  if (!action) {
161
- return { output: 'Error: action is required (searchPolymarket | searchKalshi | crossPlatform | smartMoney)', isError: true };
217
+ return {
218
+ output: 'Error: action is required (searchAll | searchPolymarket | searchKalshi | crossPlatform | leaderboard | walletProfile | smartActivity | smartMoney)',
219
+ isError: true,
220
+ };
162
221
  }
163
222
  try {
164
223
  switch (action) {
224
+ case 'searchAll': {
225
+ // One $0.005 call across 5 platforms — Polymarket, Kalshi, Limitless,
226
+ // Opinion, Predict.Fun. The right entry point for "is there a market
227
+ // on X anywhere?" — beats firing per-platform searches in parallel.
228
+ const raw = await getWithPayment('/v1/pm/markets/search', {
229
+ search,
230
+ status,
231
+ sort,
232
+ limit: cappedLimit,
233
+ }, ctx);
234
+ // Predexon returns either a flat list or per-platform buckets.
235
+ // Try the bucket shape first; fall back to a flat list.
236
+ const lines = [
237
+ `## Cross-platform market search` + (search ? ` · "${search}"` : ''),
238
+ '_Searched Polymarket, Kalshi, Limitless, Opinion, Predict.Fun in one call._',
239
+ '',
240
+ ];
241
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
242
+ const obj = raw;
243
+ const platforms = ['polymarket', 'kalshi', 'limitless', 'opinion', 'predictfun', 'predict_fun'];
244
+ let totalShown = 0;
245
+ for (const p of platforms) {
246
+ const list = unwrapList(obj[p]);
247
+ if (list.length === 0)
248
+ continue;
249
+ const remaining = cappedLimit - totalShown;
250
+ if (remaining <= 0)
251
+ break;
252
+ const shown = list.slice(0, Math.min(5, remaining));
253
+ lines.push(`### ${p}`);
254
+ shown.forEach((m, i) => {
255
+ const title = (m.title || m.question || m.market_slug || m.ticker || 'untitled');
256
+ const id = (m.condition_id || m.ticker || m.id);
257
+ const idTag = id ? ` · \`${String(id).slice(0, 18)}…\`` : '';
258
+ const vol = m.volume != null ? ` · vol ${formatUsd(m.volume)}` : '';
259
+ lines.push(`${i + 1}. ${title}${idTag}${vol}`);
260
+ totalShown++;
261
+ });
262
+ lines.push('');
263
+ }
264
+ if (totalShown === 0) {
265
+ // Bucket shape but empty — fall back to flat-list interpretation.
266
+ const flat = unwrapList(raw);
267
+ if (flat.length === 0) {
268
+ return { output: 'No markets matched across any platform.' };
269
+ }
270
+ flat.slice(0, cappedLimit).forEach((m, i) => {
271
+ const title = (m.title || m.question || m.market_slug || m.ticker || 'untitled');
272
+ const platform = (m.platform || m.source || 'unknown');
273
+ lines.push(`${i + 1}. **[${platform}]** ${title}`);
274
+ });
275
+ }
276
+ }
277
+ else {
278
+ const flat = unwrapList(raw);
279
+ if (flat.length === 0) {
280
+ return { output: 'No markets matched across any platform.' };
281
+ }
282
+ flat.slice(0, cappedLimit).forEach((m, i) => {
283
+ const title = (m.title || m.question || m.market_slug || m.ticker || 'untitled');
284
+ const platform = (m.platform || m.source || 'unknown');
285
+ lines.push(`${i + 1}. **[${platform}]** ${title}`);
286
+ });
287
+ }
288
+ lines.push(`_$0.005 paid via x402._`);
289
+ return { output: lines.join('\n') };
290
+ }
291
+ case 'leaderboard': {
292
+ // Global top-wallet ranking. Cheap ($0.001) — the right answer to
293
+ // "who's making money on Polymarket" / "who should I follow".
294
+ const raw = await getWithPayment('/v1/pm/polymarket/leaderboard', {
295
+ limit: cappedLimit,
296
+ sort,
297
+ }, ctx);
298
+ const rows = unwrapList(raw);
299
+ if (rows.length === 0) {
300
+ return { output: 'No leaderboard data returned.' };
301
+ }
302
+ const lines = [
303
+ `## Polymarket leaderboard — top ${rows.length} wallet${rows.length === 1 ? '' : 's'}`,
304
+ '',
305
+ ];
306
+ rows.forEach((r, i) => {
307
+ const wallet = (r.wallet || r.address || r.proxy_wallet || 'unknown');
308
+ const w = wallet.length > 12
309
+ ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
310
+ : wallet;
311
+ const pnl = r.pnl ?? r.realized_pnl ?? r.total_pnl;
312
+ const volume = r.volume ?? r.total_volume;
313
+ const winRate = r.win_rate ?? r.winRate;
314
+ const name = (r.name || r.handle || r.username);
315
+ const handle = name ? ` (${name})` : '';
316
+ const parts = [];
317
+ if (pnl != null)
318
+ parts.push(`P&L ${formatUsd(pnl)}`);
319
+ if (volume != null)
320
+ parts.push(`vol ${formatUsd(volume)}`);
321
+ if (winRate != null)
322
+ parts.push(`win ${formatPct(winRate, 0)}`);
323
+ lines.push(`${i + 1}. \`${w}\`${handle}` + (parts.length > 0 ? ` — ${parts.join(' · ')}` : ''));
324
+ });
325
+ lines.push('', `_$0.001 paid via x402._`);
326
+ return { output: lines.join('\n') };
327
+ }
328
+ case 'walletProfile': {
329
+ if (!wallets || !wallets.trim()) {
330
+ return {
331
+ output: 'Error: `wallets` is required for walletProfile (single address or comma-separated list of Polymarket wallet addresses)',
332
+ isError: true,
333
+ };
334
+ }
335
+ // Predexon's batch endpoint accepts multiple wallets; we forward
336
+ // verbatim. Single wallet works too — caller passes one address.
337
+ const raw = await getWithPayment('/v1/pm/polymarket/wallets/profiles', {
338
+ wallets: wallets.trim(),
339
+ }, ctx);
340
+ const profiles = unwrapList(raw);
341
+ if (profiles.length === 0) {
342
+ return { output: `No profile data returned for: ${wallets}` };
343
+ }
344
+ const lines = [
345
+ `## Polymarket wallet profile${profiles.length === 1 ? '' : 's'} — ${profiles.length}`,
346
+ '',
347
+ ];
348
+ profiles.forEach((p, i) => {
349
+ const wallet = (p.wallet || p.address || p.proxy_wallet || 'unknown');
350
+ const w = wallet.length > 12
351
+ ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
352
+ : wallet;
353
+ const name = (p.name || p.handle || p.username);
354
+ const pnl = p.pnl ?? p.realized_pnl ?? p.total_pnl;
355
+ const unrealized = p.unrealized_pnl;
356
+ const volume = p.volume ?? p.total_volume;
357
+ const positions = p.positions_count ?? p.open_positions;
358
+ const winRate = p.win_rate ?? p.winRate;
359
+ lines.push(`${i + 1}. \`${w}\`` + (name ? ` (${name})` : ''));
360
+ const stats = [];
361
+ if (pnl != null)
362
+ stats.push(`P&L ${formatUsd(pnl)}`);
363
+ if (unrealized != null)
364
+ stats.push(`unrealized ${formatUsd(unrealized)}`);
365
+ if (volume != null)
366
+ stats.push(`vol ${formatUsd(volume)}`);
367
+ if (positions != null)
368
+ stats.push(`${positions} open`);
369
+ if (winRate != null)
370
+ stats.push(`win ${formatPct(winRate, 0)}`);
371
+ if (stats.length > 0)
372
+ lines.push(` ${stats.join(' · ')}`);
373
+ });
374
+ lines.push('', `_$0.005 paid via x402._`);
375
+ return { output: lines.join('\n') };
376
+ }
377
+ case 'smartActivity': {
378
+ // "Discover markets where high-performing wallets are active right now."
379
+ // Replaces the old `smartMoney` action (which hit a non-existent path
380
+ // /v1/pm/polymarket/market/<id>/smart-money — silently 404'd from
381
+ // launch). Verified 2026-05-05 against blockrun.ai/openapi.json.
382
+ const raw = await getWithPayment('/v1/pm/polymarket/markets/smart-activity', {
383
+ limit: cappedLimit,
384
+ search,
385
+ }, ctx);
386
+ const rows = unwrapList(raw);
387
+ if (rows.length === 0) {
388
+ return { output: 'No smart-money activity recorded right now.' };
389
+ }
390
+ const lines = [
391
+ `## Smart-money activity — ${rows.length} market${rows.length === 1 ? '' : 's'}`,
392
+ '_Markets where high-P&L Polymarket wallets are positioning right now._',
393
+ '',
394
+ ];
395
+ rows.forEach((r, i) => {
396
+ const title = (r.question || r.title || r.market_slug || 'untitled');
397
+ const cid = (r.condition_id || r.id);
398
+ const cidTag = cid ? ` · \`${String(cid).slice(0, 14)}…\`` : '';
399
+ const smartCount = r.smart_wallets_count ?? r.wallet_count;
400
+ const netFlow = r.net_size ?? r.net_yes_size;
401
+ const stats = [];
402
+ if (smartCount != null)
403
+ stats.push(`${smartCount} smart wallet${smartCount === 1 ? '' : 's'}`);
404
+ if (netFlow != null)
405
+ stats.push(`net ${formatUsd(netFlow)}`);
406
+ lines.push(`${i + 1}. **${title}**${cidTag}` + (stats.length > 0 ? `\n ${stats.join(' · ')}` : ''));
407
+ });
408
+ lines.push('', `_$0.005 paid via x402._`);
409
+ return { output: lines.join('\n') };
410
+ }
411
+ case 'smartMoney': {
412
+ if (!conditionId) {
413
+ return {
414
+ output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket or smartActivity call)',
415
+ isError: true,
416
+ };
417
+ }
418
+ // Per-market drill-down. Official live registry:
419
+ // /api/v1/pm/polymarket/market/:condition_id/smart-money
420
+ const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
421
+ const data = await getWithPayment(path, {}, ctx);
422
+ const buyers = (data.buyers ?? []).slice(0, 5);
423
+ const sellers = (data.sellers ?? []).slice(0, 5);
424
+ const lines = [
425
+ `## Smart money — \`${conditionId.slice(0, 14)}…\``,
426
+ ];
427
+ if (data.net_yes_size != null || data.net_no_size != null) {
428
+ lines.push(`**Net flow:** YES ${formatUsd(data.net_yes_size)} / NO ${formatUsd(data.net_no_size)}`);
429
+ }
430
+ if (buyers.length > 0) {
431
+ lines.push('', '**Top buyers**');
432
+ buyers.forEach((b, i) => {
433
+ const w = b.wallet ? `${b.wallet.slice(0, 8)}…${b.wallet.slice(-4)}` : 'unknown';
434
+ lines.push(`${i + 1}. ${w} — ${formatUsd(b.size)} on ${b.outcome ?? 'unknown side'}`);
435
+ });
436
+ }
437
+ if (sellers.length > 0) {
438
+ lines.push('', '**Top sellers**');
439
+ sellers.forEach((s, i) => {
440
+ const w = s.wallet ? `${s.wallet.slice(0, 8)}…${s.wallet.slice(-4)}` : 'unknown';
441
+ lines.push(`${i + 1}. ${w} — ${formatUsd(s.size)} on ${s.outcome ?? 'unknown side'}`);
442
+ });
443
+ }
444
+ if (buyers.length === 0 && sellers.length === 0) {
445
+ lines.push('No smart-money flow recorded for this market yet.');
446
+ }
447
+ lines.push('', `_$0.005 paid via x402._`);
448
+ return { output: lines.join('\n') };
449
+ }
165
450
  case 'searchPolymarket': {
166
451
  const raw = await getWithPayment('/v1/pm/polymarket/markets', {
167
452
  search,
@@ -245,45 +530,9 @@ async function execute(input, ctx) {
245
530
  lines.push('', `_$0.005 paid via x402._`);
246
531
  return { output: lines.join('\n') };
247
532
  }
248
- case 'smartMoney': {
249
- if (!conditionId) {
250
- return { output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket call)', isError: true };
251
- }
252
- const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
253
- const data = await getWithPayment(path, {}, ctx);
254
- const buyers = (data.buyers ?? []).slice(0, 5);
255
- const sellers = (data.sellers ?? []).slice(0, 5);
256
- const lines = [
257
- `## Smart money — \`${conditionId.slice(0, 14)}…\``,
258
- ];
259
- if (data.net_yes_size != null || data.net_no_size != null) {
260
- const yesSize = formatUsd(data.net_yes_size);
261
- const noSize = formatUsd(data.net_no_size);
262
- lines.push(`**Net flow:** YES ${yesSize} / NO ${noSize}`);
263
- }
264
- if (buyers.length > 0) {
265
- lines.push('', '**Top buyers**');
266
- buyers.forEach((b, i) => {
267
- const w = b.wallet ? `${b.wallet.slice(0, 8)}…${b.wallet.slice(-4)}` : 'unknown';
268
- lines.push(`${i + 1}. ${w} — ${formatUsd(b.size)} on ${b.outcome ?? 'unknown side'}`);
269
- });
270
- }
271
- if (sellers.length > 0) {
272
- lines.push('', '**Top sellers**');
273
- sellers.forEach((s, i) => {
274
- const w = s.wallet ? `${s.wallet.slice(0, 8)}…${s.wallet.slice(-4)}` : 'unknown';
275
- lines.push(`${i + 1}. ${w} — ${formatUsd(s.size)} on ${s.outcome ?? 'unknown side'}`);
276
- });
277
- }
278
- if (buyers.length === 0 && sellers.length === 0) {
279
- lines.push('No smart-money flow recorded for this market yet.');
280
- }
281
- lines.push('', `_$0.005 paid via x402._`);
282
- return { output: lines.join('\n') };
283
- }
284
533
  default:
285
534
  return {
286
- output: `Error: unknown action "${action}". Use: searchPolymarket, searchKalshi, crossPlatform, smartMoney`,
535
+ output: `Error: unknown action "${action}". Use: searchAll, searchPolymarket, searchKalshi, crossPlatform, leaderboard, walletProfile, smartActivity, smartMoney`,
287
536
  isError: true,
288
537
  };
289
538
  }
@@ -295,41 +544,63 @@ async function execute(input, ctx) {
295
544
  export const predictionMarketCapability = {
296
545
  spec: {
297
546
  name: 'PredictionMarket',
298
- description: 'Real prediction market data via the BlockRun gateway (powered by Predexon). Use for any "what are the odds of X" / "Polymarket on Y" / "Kalshi market for Z" question. ' +
547
+ 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. ' +
299
548
  'Actions: ' +
300
- '`searchPolymarket` (search Polymarket markets — $0.001), ' +
301
- '`searchKalshi` (search Kalshi markets — $0.001), ' +
302
- '`crossPlatform` (matched pairs across Polymarket+Kalshi for arbitrage / consensus — $0.005), ' +
303
- '`smartMoney` (smart-money positioning on a Polymarket condition_id — $0.005). ' +
304
- 'Polymarket and Kalshi are the two largest legit prediction markets; cross-platform matching is unique to BlockRun. ' +
305
- 'For "should I bet on X" / "what does the market price imply": run searchPolymarket AND searchKalshi in parallel and compare implied probabilities divergence is the signal.',
549
+ '`searchAll` (search markets across Polymarket+Kalshi+Limitless+Opinion+Predict.Fun in one call — $0.005), ' +
550
+ '`searchPolymarket` (Polymarket only, supports sort+status — $0.001), ' +
551
+ '`searchKalshi` (Kalshi only, supports sort+status — $0.001), ' +
552
+ '`crossPlatform` (matched market pairs across Polymarket+Kalshi for arbitrage / consensus — $0.005), ' +
553
+ '`leaderboard` (global top wallets by P&L on Polymarket $0.001), ' +
554
+ '`walletProfile` (P&L + positions for one or more Polymarket wallets$0.005), ' +
555
+ '`smartActivity` (markets where high-P&L wallets are positioning right now — $0.005), ' +
556
+ '`smartMoney` (smart-money positioning on one Polymarket condition_id — $0.005). ' +
557
+ 'Default routing: ' +
558
+ '"is there a market on X anywhere" → searchAll. ' +
559
+ '"top wallets / who is profitable / who should I follow on Polymarket" → leaderboard. ' +
560
+ '"how is wallet 0xabc doing / show me their P&L" → walletProfile with that address. ' +
561
+ '"what are smart traders betting on right now" → smartActivity. ' +
562
+ '"show smart money on this specific Polymarket market" → smartMoney with conditionId. ' +
563
+ '"should I bet on X" → run searchPolymarket + searchKalshi in parallel and compare implied probabilities — divergence is the signal.',
306
564
  input_schema: {
307
565
  type: 'object',
308
566
  properties: {
309
567
  action: {
310
568
  type: 'string',
311
- enum: ['searchPolymarket', 'searchKalshi', 'crossPlatform', 'smartMoney'],
569
+ enum: [
570
+ 'searchAll',
571
+ 'searchPolymarket',
572
+ 'searchKalshi',
573
+ 'crossPlatform',
574
+ 'leaderboard',
575
+ 'walletProfile',
576
+ 'smartActivity',
577
+ 'smartMoney',
578
+ ],
312
579
  description: 'Which prediction-market query to run. See tool description for cost per action.',
313
580
  },
314
581
  search: {
315
582
  type: 'string',
316
- description: 'Search query (3-100 chars). Used by searchPolymarket / searchKalshi. Skip for crossPlatform/smartMoney.',
583
+ description: 'Search query. Used by searchAll / searchPolymarket / searchKalshi / smartActivity. Optional for crossPlatform/leaderboard/walletProfile/smartMoney.',
317
584
  },
318
585
  status: {
319
586
  type: 'string',
320
- description: 'Polymarket: active | closed | archived (default active). Kalshi: open | closed (default open).',
587
+ description: 'Polymarket: active | closed | archived (default active). Kalshi: open | closed (default open). Forwarded to searchAll where supported.',
321
588
  },
322
589
  sort: {
323
590
  type: 'string',
324
- description: 'Polymarket: volume | liquidity | created (default volume). Kalshi: volume | open_interest | price_desc | price_asc | close_time (default volume).',
591
+ 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).',
325
592
  },
326
593
  limit: {
327
594
  type: 'number',
328
595
  description: `Max results (default ${DEFAULT_LIMIT}, hard cap ${MAX_LIMIT}).`,
329
596
  },
597
+ wallets: {
598
+ type: 'string',
599
+ description: 'For walletProfile: a single Polymarket wallet address, or a comma-separated list of addresses for batch lookup.',
600
+ },
330
601
  conditionId: {
331
602
  type: 'string',
332
- description: 'Polymarket condition_id. Required for smartMoney. Get one from a prior searchPolymarket call.',
603
+ description: 'For smartMoney: Polymarket condition_id from searchPolymarket or smartActivity.',
333
604
  },
334
605
  },
335
606
  required: ['action'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.69",
3
+ "version": "3.15.71",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {