@blockrun/franklin 3.15.72 → 3.15.74

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,12 @@ 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. Eight actions, route by intent:
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.
343
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).
344
+ - "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 = full picture for \$0.015. Do NOT \`Bash\`-curl \`data-api.polymarket.com\` directly — those are paid Predexon endpoints and going around them defeats the wallet-attached architecture. If just the profile is needed: \`walletProfile\` alone (single address → /wallet/{addr}, comma-list batch).
345
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
346
  - "show smart money on this specific Polymarket market / this condition_id" → \`smartMoney\` (\$0.005) with \`conditionId="<condition_id>"\`.
347
347
 
@@ -17,8 +17,12 @@
17
17
  * crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
18
18
  * (the arbitrage / consensus signal)
19
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
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
22
26
  * smartActivity $0.005 discover markets where high-performing wallets
23
27
  * are active right now
24
28
  * smartMoney $0.005 smart-money positioning on one Polymarket
@@ -17,8 +17,12 @@
17
17
  * crossPlatform $0.005 matching market pairs across Polymarket+Kalshi
18
18
  * (the arbitrage / consensus signal)
19
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
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
22
26
  * smartActivity $0.005 discover markets where high-performing wallets
23
27
  * are active right now
24
28
  * smartMoney $0.005 smart-money positioning on one Polymarket
@@ -98,7 +102,7 @@ async function getWithPayment(path, query, ctx) {
98
102
  const errText = await response.text().catch(() => '');
99
103
  // Surface failed paid calls in the Markets-tab health summary.
100
104
  recordFetch({ provider: 'blockrun', endpoint: path, ok: false, latencyMs: Date.now() - startedAt });
101
- throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 200)}`);
105
+ throw new Error(`PredictionMarket ${path} failed (${response.status}): ${errText.slice(0, 600)}`);
102
106
  }
103
107
  recordFetch({
104
108
  provider: 'blockrun',
@@ -186,11 +190,18 @@ function formatUsd(value) {
186
190
  return `$${(n / 1e3).toFixed(1)}K`;
187
191
  return `$${n.toFixed(2)}`;
188
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
+ }
189
199
  function formatPct(value, digits = 1) {
190
200
  const n = asNumber(value);
191
201
  if (n == null)
192
202
  return 'n/a';
193
- return `${(n * 100).toFixed(digits)}%`;
203
+ const pct = Math.abs(n) > 1 ? n : n * 100;
204
+ return `${pct.toFixed(digits)}%`;
194
205
  }
195
206
  // API responses sometimes come wrapped as `{data: [...], pagination: ...}`,
196
207
  // other times as a bare array. Normalise to an array.
@@ -207,15 +218,23 @@ function unwrapList(raw) {
207
218
  return obj.pairs;
208
219
  if (Array.isArray(obj.results))
209
220
  return obj.results;
221
+ if (Array.isArray(obj.positions))
222
+ return obj.positions;
210
223
  }
211
224
  return [];
212
225
  }
226
+ function parseWalletsInput(value) {
227
+ return value
228
+ .split(',')
229
+ .map(w => w.trim())
230
+ .filter(Boolean);
231
+ }
213
232
  async function execute(input, ctx) {
214
- const { action, search, status, sort, limit, conditionId, wallets } = input;
233
+ const { action, search, status, sort, limit, conditionId, wallets, granularity } = input;
215
234
  const cappedLimit = Math.min(Math.max(1, limit ?? DEFAULT_LIMIT), MAX_LIMIT);
216
235
  if (!action) {
217
236
  return {
218
- output: 'Error: action is required (searchAll | searchPolymarket | searchKalshi | crossPlatform | leaderboard | walletProfile | smartActivity | smartMoney)',
237
+ output: 'Error: action is required (searchAll | searchPolymarket | searchKalshi | crossPlatform | leaderboard | walletProfile | walletPnl | walletPositions | smartActivity | smartMoney)',
219
238
  isError: true,
220
239
  };
221
240
  }
@@ -225,8 +244,12 @@ async function execute(input, ctx) {
225
244
  // One $0.005 call across 5 platforms — Polymarket, Kalshi, Limitless,
226
245
  // Opinion, Predict.Fun. The right entry point for "is there a market
227
246
  // on X anywhere?" — beats firing per-platform searches in parallel.
247
+ // Predexon expects `q` for the search term — verified 2026-05-06 from
248
+ // a live 422: {"detail":[{"type":"missing","loc":["query","q"]}]}.
249
+ // Public input field stays `search` for ergonomic consistency with
250
+ // searchPolymarket / searchKalshi; rename on the wire.
228
251
  const raw = await getWithPayment('/v1/pm/markets/search', {
229
- search,
252
+ q: search,
230
253
  status,
231
254
  sort,
232
255
  limit: cappedLimit,
@@ -332,17 +355,33 @@ async function execute(input, ctx) {
332
355
  isError: true,
333
356
  };
334
357
  }
335
- // Predexon's batch endpoint expects the query param `addresses`,
336
- // NOT `wallets` verified 2026-05-06 from a live 422 in a real
337
- // user session: `{"detail":[{"type":"missing","loc":["query",
338
- // "addresses"]}]}`. The 3.15.70 ship guessed the param name from
339
- // the openapi description ("Batch retrieve wallet profiles") and
340
- // got it wrong. Public field stays `wallets` for ergonomics —
341
- // we just rename on the wire.
342
- const raw = await getWithPayment('/v1/pm/polymarket/wallets/profiles', {
343
- addresses: wallets.trim(),
344
- }, ctx);
345
- const profiles = unwrapList(raw);
358
+ // Smart dispatch: a single wallet /wallet/{addr} (full profile,
359
+ // labels, scores, stats); a comma-list /wallets/profiles (batch).
360
+ // The 3.15.70 ship hit the BATCH endpoint for everything and got 422
361
+ // for the single-wallet case; the gateway team confirmed 2026-05-06
362
+ // the right surface for "analyze this trader" is the path-parameter
363
+ // single-wallet endpoint, not the batch query-param one.
364
+ const parsedWallets = parseWalletsInput(wallets);
365
+ if (parsedWallets.length === 0) {
366
+ return {
367
+ output: 'Error: `wallets` must include at least one Polymarket wallet address',
368
+ isError: true,
369
+ };
370
+ }
371
+ const list = parsedWallets.join(',');
372
+ const isBatch = parsedWallets.length > 1;
373
+ const raw = isBatch
374
+ ? await getWithPayment('/v1/pm/polymarket/wallets/profiles', {
375
+ addresses: list,
376
+ }, ctx)
377
+ : await getWithPayment(`/v1/pm/polymarket/wallet/${encodeURIComponent(list)}`, {}, ctx);
378
+ // Single-wallet path returns a single profile object; batch returns
379
+ // an array (or {data:[]}). unwrapList handles the batch shape but
380
+ // returns [] for a bare object — wrap explicitly so the formatter
381
+ // below sees the single profile.
382
+ const profiles = isBatch
383
+ ? unwrapList(raw)
384
+ : (raw && typeof raw === 'object' ? [raw] : []);
346
385
  if (profiles.length === 0) {
347
386
  return { output: `No profile data returned for: ${wallets}` };
348
387
  }
@@ -379,11 +418,124 @@ async function execute(input, ctx) {
379
418
  lines.push('', `_$0.005 paid via x402._`);
380
419
  return { output: lines.join('\n') };
381
420
  }
421
+ case 'walletPnl': {
422
+ // Single-wallet P&L summary + time series.
423
+ // Predexon path: /v1/pm/polymarket/wallet/pnl/{wallet} — Tier 2 ($0.005).
424
+ if (!wallets || !wallets.trim()) {
425
+ return {
426
+ output: 'Error: `wallets` is required for walletPnl (single Polymarket wallet address)',
427
+ isError: true,
428
+ };
429
+ }
430
+ const parsedWallets = parseWalletsInput(wallets);
431
+ if (parsedWallets.length !== 1) {
432
+ return {
433
+ output: 'Error: walletPnl accepts exactly one wallet address. For multiple wallets, call walletPnl once per address in parallel.',
434
+ isError: true,
435
+ };
436
+ }
437
+ const wallet = parsedWallets[0];
438
+ // Predexon requires `granularity` from the enum {day, week, month,
439
+ // year, all} — verified 2026-05-06 in two live 422 turns. Default
440
+ // `day`; agent can override via input field for longer aggregations.
441
+ const raw = await getWithPayment(`/v1/pm/polymarket/wallet/pnl/${encodeURIComponent(wallet)}`, { granularity: granularity ?? 'day' }, ctx);
442
+ if (!raw || typeof raw !== 'object') {
443
+ return { output: `No P&L data returned for ${wallet}` };
444
+ }
445
+ const data = raw;
446
+ const realized = data.realized_pnl ?? data.realizedPnl ?? data.total_pnl ?? data.pnl;
447
+ const unrealized = data.unrealized_pnl ?? data.unrealizedPnl;
448
+ const total = data.total_value ?? data.totalValue ?? data.equity;
449
+ const volume = data.volume ?? data.total_volume;
450
+ const winRate = data.win_rate ?? data.winRate;
451
+ const w = wallet.length > 12 ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}` : wallet;
452
+ const lines = [`## Polymarket wallet P&L — \`${w}\``, ''];
453
+ const summary = [];
454
+ if (realized != null)
455
+ summary.push(`realized ${formatUsd(realized)}`);
456
+ if (unrealized != null)
457
+ summary.push(`unrealized ${formatUsd(unrealized)}`);
458
+ if (total != null)
459
+ summary.push(`equity ${formatUsd(total)}`);
460
+ if (volume != null)
461
+ summary.push(`vol ${formatUsd(volume)}`);
462
+ if (winRate != null)
463
+ summary.push(`win ${formatPct(winRate, 0)}`);
464
+ if (summary.length > 0)
465
+ lines.push(summary.join(' · '));
466
+ // Optional time series — show recent points compactly if present.
467
+ const series = (data.series ?? data.history ?? data.daily);
468
+ if (Array.isArray(series) && series.length > 0) {
469
+ lines.push('', `**Recent points** (latest ${Math.min(7, series.length)}):`);
470
+ series.slice(-7).forEach(pt => {
471
+ const t = (pt.date ?? pt.ts ?? pt.timestamp);
472
+ const v = (pt.pnl ?? pt.value ?? pt.cumulative_pnl);
473
+ if (t != null && v != null) {
474
+ const tStr = typeof t === 'number' ? new Date(t).toISOString().slice(0, 10) : String(t).slice(0, 10);
475
+ lines.push(`- ${tStr} · ${formatUsd(v)}`);
476
+ }
477
+ });
478
+ }
479
+ lines.push('', `_$0.005 paid via x402._`);
480
+ return { output: lines.join('\n') };
481
+ }
482
+ case 'walletPositions': {
483
+ // Single-wallet positions (open + historical).
484
+ // Predexon path: /v1/pm/polymarket/wallet/positions/{wallet} — Tier 2 ($0.005).
485
+ if (!wallets || !wallets.trim()) {
486
+ return {
487
+ output: 'Error: `wallets` is required for walletPositions (single Polymarket wallet address)',
488
+ isError: true,
489
+ };
490
+ }
491
+ const parsedWallets = parseWalletsInput(wallets);
492
+ if (parsedWallets.length !== 1) {
493
+ return {
494
+ output: 'Error: walletPositions accepts exactly one wallet address. For multiple wallets, call walletPositions once per address in parallel.',
495
+ isError: true,
496
+ };
497
+ }
498
+ const wallet = parsedWallets[0];
499
+ const raw = await getWithPayment(`/v1/pm/polymarket/wallet/positions/${encodeURIComponent(wallet)}`, { limit: cappedLimit }, ctx);
500
+ const positions = unwrapList(raw);
501
+ if (positions.length === 0) {
502
+ return { output: `No positions returned for ${wallet}` };
503
+ }
504
+ const w = wallet.length > 12 ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}` : wallet;
505
+ const lines = [
506
+ `## Polymarket positions — \`${w}\` — ${positions.length} position${positions.length === 1 ? '' : 's'}`,
507
+ '',
508
+ ];
509
+ positions.slice(0, cappedLimit).forEach((p, i) => {
510
+ const title = (p.title || p.market || p.question || p.market_slug || 'untitled');
511
+ const outcome = (p.outcome || p.side);
512
+ const size = p.size ?? p.shares ?? p.quantity;
513
+ const avgPrice = p.avg_price ?? p.avgPrice ?? p.average_price;
514
+ const currentValue = p.current_value ?? p.currentValue ?? p.value;
515
+ const pnl = p.pnl ?? p.unrealized_pnl ?? p.realized_pnl;
516
+ const pnlPct = p.pnl_pct ?? p.pnlPct ?? p.percent_pnl;
517
+ const parts = [];
518
+ if (outcome)
519
+ parts.push(outcome);
520
+ if (size != null)
521
+ parts.push(`size ${formatQuantity(size)}`);
522
+ if (avgPrice != null)
523
+ parts.push(`avg ${formatPct(avgPrice)}`);
524
+ if (currentValue != null)
525
+ parts.push(`now ${formatUsd(currentValue)}`);
526
+ if (pnl != null) {
527
+ const pctStr = pnlPct != null ? ` (${formatPct(pnlPct, 1)})` : '';
528
+ parts.push(`P&L ${formatUsd(pnl)}${pctStr}`);
529
+ }
530
+ lines.push(`${i + 1}. **${title}** — ${parts.join(' · ')}`);
531
+ });
532
+ lines.push('', `_$0.005 paid via x402._`);
533
+ return { output: lines.join('\n') };
534
+ }
382
535
  case 'smartActivity': {
383
536
  // "Discover markets where high-performing wallets are active right now."
384
- // Replaces the old `smartMoney` action (which hit a non-existent path
385
- // /v1/pm/polymarket/market/<id>/smart-money silently 404'd from
386
- // launch). Verified 2026-05-05 against blockrun.ai/openapi.json.
537
+ // Complements `smartMoney`: this discovers interesting markets across
538
+ // the venue; smartMoney drills into one condition_id.
387
539
  const raw = await getWithPayment('/v1/pm/polymarket/markets/smart-activity', {
388
540
  limit: cappedLimit,
389
541
  search,
@@ -537,7 +689,7 @@ async function execute(input, ctx) {
537
689
  }
538
690
  default:
539
691
  return {
540
- output: `Error: unknown action "${action}". Use: searchAll, searchPolymarket, searchKalshi, crossPlatform, leaderboard, walletProfile, smartActivity, smartMoney`,
692
+ output: `Error: unknown action "${action}". Use: searchAll, searchPolymarket, searchKalshi, crossPlatform, leaderboard, walletProfile, walletPnl, walletPositions, smartActivity, smartMoney`,
541
693
  isError: true,
542
694
  };
543
695
  }
@@ -556,13 +708,15 @@ export const predictionMarketCapability = {
556
708
  '`searchKalshi` (Kalshi only, supports sort+status — $0.001), ' +
557
709
  '`crossPlatform` (matched market pairs across Polymarket+Kalshi for arbitrage / consensus — $0.005), ' +
558
710
  '`leaderboard` (global top wallets by P&L on Polymarket — $0.001), ' +
559
- '`walletProfile` (P&L + positions for one or more Polymarket wallets — $0.005), ' +
711
+ '`walletProfile` (full Polymarket wallet profile labels, scores, stats. Single address → /wallet/{addr}; comma-list → batch /wallets/profiles — $0.005), ' +
712
+ '`walletPnl` (single Polymarket wallet P&L summary + time series — $0.005), ' +
713
+ '`walletPositions` (single Polymarket wallet positions — open + historical with P&L per position — $0.005), ' +
560
714
  '`smartActivity` (markets where high-P&L wallets are positioning right now — $0.005), ' +
561
715
  '`smartMoney` (smart-money positioning on one Polymarket condition_id — $0.005). ' +
562
716
  'Default routing: ' +
563
717
  '"is there a market on X anywhere" → searchAll. ' +
564
718
  '"top wallets / who is profitable / who should I follow on Polymarket" → leaderboard. ' +
565
- '"how is wallet 0xabc doing / show me their P&L" → walletProfile with that address. ' +
719
+ '"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. ' +
566
720
  '"what are smart traders betting on right now" → smartActivity. ' +
567
721
  '"show smart money on this specific Polymarket market" → smartMoney with conditionId. ' +
568
722
  '"should I bet on X" → run searchPolymarket + searchKalshi in parallel and compare implied probabilities — divergence is the signal.',
@@ -578,6 +732,8 @@ export const predictionMarketCapability = {
578
732
  'crossPlatform',
579
733
  'leaderboard',
580
734
  'walletProfile',
735
+ 'walletPnl',
736
+ 'walletPositions',
581
737
  'smartActivity',
582
738
  'smartMoney',
583
739
  ],
@@ -585,7 +741,7 @@ export const predictionMarketCapability = {
585
741
  },
586
742
  search: {
587
743
  type: 'string',
588
- description: 'Search query. Used by searchAll / searchPolymarket / searchKalshi / smartActivity. Optional for crossPlatform/leaderboard/walletProfile/smartMoney.',
744
+ description: 'Search query. Used by searchAll / searchPolymarket / searchKalshi / smartActivity. Optional for crossPlatform/leaderboard/walletProfile/walletPnl/walletPositions/smartMoney.',
589
745
  },
590
746
  status: {
591
747
  type: 'string',
@@ -607,6 +763,11 @@ export const predictionMarketCapability = {
607
763
  type: 'string',
608
764
  description: 'For smartMoney: Polymarket condition_id from searchPolymarket or smartActivity.',
609
765
  },
766
+ granularity: {
767
+ type: 'string',
768
+ enum: ['day', 'week', 'month', 'year', 'all'],
769
+ description: 'For walletPnl: time bucket for the P&L series. Default day.',
770
+ },
610
771
  },
611
772
  required: ['action'],
612
773
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.72",
3
+ "version": "3.15.74",
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": {