@blockrun/franklin 3.21.6 → 3.21.7

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.
@@ -46,7 +46,15 @@ import { createSessionId, appendToSession, updateSessionMeta, pruneOldSessions,
46
46
  function replaceHistory(target, replacement) {
47
47
  target.splice(0, target.length, ...replacement);
48
48
  }
49
- const EXTERNAL_WALL_FAILURE_PATTERN = /\b(?:401|403|429|5\d{2})\b|\bunauthor|\bforbid|\bWAF\b|\bcloudflare\b|\bfault filter\b|\bblocked\b|\binvalid (?:auth|api|token|key|bearer)\b/i;
49
+ // 400/422 (malformed request / failed param validation) are added alongside
50
+ // 401/403/429/5xx: like an auth wall, retrying the same bad request never
51
+ // recovers — the agent must change its inputs, not hammer the endpoint. Caught
52
+ // 2026-05-20 when a `status=active` 422 (invalid enum, Predexon wants
53
+ // open|closed) spun PredictionMarket to the 50-call HARD_TOOL_CAP because the
54
+ // 422 was neither charged (cost guard idle) nor matched here (wall guard idle).
55
+ // 404 is intentionally excluded — "not found" is a legitimate cue to retry
56
+ // with a different query, not a dead wall.
57
+ const EXTERNAL_WALL_FAILURE_PATTERN = /\b(?:400|401|403|422|429|5\d{2})\b|\bunauthor|\bforbid|\bWAF\b|\bcloudflare\b|\bfault filter\b|\bblocked\b|\binvalid (?:auth|api|token|key|bearer)\b/i;
50
58
  export function isExternalWallFailure(toolName, output, isError) {
51
59
  if (toolName === 'WebFetch') {
52
60
  return isError === true || EXTERNAL_WALL_FAILURE_PATTERN.test(output);
@@ -220,6 +220,8 @@ function unwrapList(raw) {
220
220
  return obj.results;
221
221
  if (Array.isArray(obj.positions))
222
222
  return obj.positions;
223
+ if (Array.isArray(obj.entries))
224
+ return obj.entries; // leaderboard
223
225
  }
224
226
  return [];
225
227
  }
@@ -229,6 +231,46 @@ function parseWalletsInput(value) {
229
231
  .map(w => w.trim())
230
232
  .filter(Boolean);
231
233
  }
234
+ // Predexon's market-list endpoints (polymarket/markets, kalshi/markets,
235
+ // markets/search) all validate `status` against StatusOption = {open, closed}
236
+ // — verified against openapi-v2.json. Earlier code defaulted Polymarket to
237
+ // `active` (the Polymarket Gamma-API convention), which 422s on Predexon.
238
+ // Normalize the common synonyms so an explicit `active` / `archived` coming
239
+ // from the agent or the user still resolves to a valid value.
240
+ // Polymarket leaderboard `sort_by` enum (openapi-v2.json:
241
+ // realized_pnl | total_pnl | volume | roi | profit_factor | win_rate | trades).
242
+ // Maps the ergonomic values the tool advertises ("pnl | volume | win_rate")
243
+ // — and a few obvious synonyms — onto the real enum. Unknown keys resolve to
244
+ // undefined so the param is omitted and the gateway uses its own default.
245
+ const LEADERBOARD_SORT_ALIASES = {
246
+ pnl: 'total_pnl',
247
+ total_pnl: 'total_pnl',
248
+ realized_pnl: 'realized_pnl',
249
+ realized: 'realized_pnl',
250
+ volume: 'volume',
251
+ roi: 'roi',
252
+ profit_factor: 'profit_factor',
253
+ win_rate: 'win_rate',
254
+ winrate: 'win_rate',
255
+ trades: 'trades',
256
+ };
257
+ // smartActivity and smart-money both REQUIRE at least one smart-wallet
258
+ // criterion (min_realized_pnl / min_total_pnl / min_roi / …) or Predexon 400s
259
+ // with "At least one smart wallet criterion must be specified" — verified live
260
+ // 2026-05-20. The old code sent none, so both endpoints failed every call.
261
+ // Default to a sensible "smart = profitable" floor; the agent can still pass
262
+ // its own threshold via the `minTotalPnl` input.
263
+ const DEFAULT_SMART_MIN_TOTAL_PNL = 10000;
264
+ function normalizeMarketStatus(status) {
265
+ if (!status)
266
+ return status;
267
+ const s = status.trim().toLowerCase();
268
+ if (s === 'active' || s === 'open' || s === 'live')
269
+ return 'open';
270
+ if (s === 'closed' || s === 'archived' || s === 'resolved' || s === 'inactive')
271
+ return 'closed';
272
+ return s;
273
+ }
232
274
  /**
233
275
  * Pick the first usable string from a list of candidate values.
234
276
  *
@@ -286,7 +328,7 @@ async function execute(input, ctx) {
286
328
  // searchPolymarket / searchKalshi; rename on the wire.
287
329
  const raw = await getWithPayment('/v1/pm/markets/search', {
288
330
  q: search,
289
- status,
331
+ status: normalizeMarketStatus(status),
290
332
  sort,
291
333
  limit: cappedLimit,
292
334
  }, ctx);
@@ -313,7 +355,7 @@ async function execute(input, ctx) {
313
355
  shown.forEach((m, i) => {
314
356
  const title = pickString(m.title, m.question, m.market, m.event, m.market_slug, m.slug, m.ticker) ?? 'untitled';
315
357
  const id = pickString(m.condition_id, m.ticker, m.id);
316
- const idTag = id ? ` · \`${String(id).slice(0, 18)}…\`` : '';
358
+ const idTag = id ? ` · \`${String(id)}\`` : '';
317
359
  const vol = m.volume != null ? ` · vol ${formatUsd(m.volume)}` : '';
318
360
  lines.push(`${i + 1}. ${title}${idTag}${vol}`);
319
361
  totalShown++;
@@ -350,9 +392,15 @@ async function execute(input, ctx) {
350
392
  case 'leaderboard': {
351
393
  // Global top-wallet ranking. Cheap ($0.001) — the right answer to
352
394
  // "who's making money on Polymarket" / "who should I follow".
395
+ // Predexon's param is `sort_by` (enum), NOT `sort` — sending `sort`
396
+ // is silently ignored, so the ranking never honored the agent's
397
+ // intent. Map the ergonomic aliases to the real enum values
398
+ // (verified against openapi-v2.json), drop anything unrecognized so
399
+ // the gateway falls back to its own default rather than 422-ing.
400
+ const sortBy = sort ? LEADERBOARD_SORT_ALIASES[sort.trim().toLowerCase()] : undefined;
353
401
  const raw = await getWithPayment('/v1/pm/polymarket/leaderboard', {
354
402
  limit: cappedLimit,
355
- sort,
403
+ sort_by: sortBy,
356
404
  }, ctx);
357
405
  const rows = unwrapList(raw);
358
406
  if (rows.length === 0) {
@@ -363,13 +411,18 @@ async function execute(input, ctx) {
363
411
  '',
364
412
  ];
365
413
  rows.forEach((r, i) => {
366
- const wallet = pickString(r.wallet, r.address, r.proxy_wallet, r.proxyWallet) ?? 'unknown';
414
+ // Predexon v2 entry: { rank, user, metrics:{ total_pnl, volume,
415
+ // win_rate, ... }, ... } — verified live 2026-05-20. P&L/volume/win
416
+ // live under `metrics`, the address is `user`; the old flat
417
+ // r.pnl / r.wallet reads returned undefined for every row.
418
+ const metrics = (r.metrics && typeof r.metrics === 'object' ? r.metrics : {});
419
+ const wallet = pickString(r.user, r.wallet, r.address, r.proxy_wallet, r.proxyWallet) ?? 'unknown';
367
420
  const w = wallet.length > 12
368
421
  ? `${wallet.slice(0, 8)}…${wallet.slice(-4)}`
369
422
  : 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;
423
+ const pnl = metrics.total_pnl ?? metrics.realized_pnl ?? r.pnl ?? r.realized_pnl ?? r.total_pnl;
424
+ const volume = metrics.volume ?? r.volume ?? r.total_volume;
425
+ const winRate = metrics.win_rate ?? r.win_rate ?? r.winRate;
373
426
  const name = pickString(r.name, r.handle, r.username);
374
427
  const handle = name ? ` (${name})` : '';
375
428
  const parts = [];
@@ -645,9 +698,11 @@ async function execute(input, ctx) {
645
698
  // "Discover markets where high-performing wallets are active right now."
646
699
  // Complements `smartMoney`: this discovers interesting markets across
647
700
  // the venue; smartMoney drills into one condition_id.
701
+ // Requires a smart-wallet criterion (else 400). `search` is not a
702
+ // supported param here, so it was silently dropped — removed.
648
703
  const raw = await getWithPayment('/v1/pm/polymarket/markets/smart-activity', {
649
704
  limit: cappedLimit,
650
- search,
705
+ min_total_pnl: DEFAULT_SMART_MIN_TOTAL_PNL,
651
706
  }, ctx);
652
707
  const rows = unwrapList(raw);
653
708
  if (rows.length === 0) {
@@ -661,14 +716,20 @@ async function execute(input, ctx) {
661
716
  rows.forEach((r, i) => {
662
717
  const title = pickString(r.question, r.title, r.market, r.event, r.market_slug, r.slug) ?? 'untitled';
663
718
  const cid = pickString(r.condition_id, r.id);
664
- const cidTag = cid ? ` · \`${String(cid).slice(0, 14)}…\`` : '';
665
- const smartCount = r.smart_wallets_count ?? r.wallet_count;
666
- const netFlow = r.net_size ?? r.net_yes_size;
719
+ // Full condition_id so the agent can chain into smartMoney.
720
+ const cidTag = cid ? ` · \`${String(cid)}\`` : '';
721
+ // Predexon v2 fields (verified live): smart_wallet_count (singular),
722
+ // smart_volume, net_buyers_pct.
723
+ const smartCount = r.smart_wallet_count ?? r.smart_wallets_count ?? r.wallet_count;
724
+ const smartVol = r.smart_volume ?? r.net_size ?? r.net_yes_size;
725
+ const netBuyersPct = r.net_buyers_pct;
667
726
  const stats = [];
668
727
  if (smartCount != null)
669
728
  stats.push(`${smartCount} smart wallet${smartCount === 1 ? '' : 's'}`);
670
- if (netFlow != null)
671
- stats.push(`net ${formatUsd(netFlow)}`);
729
+ if (smartVol != null)
730
+ stats.push(`smart vol ${formatUsd(smartVol)}`);
731
+ if (netBuyersPct != null)
732
+ stats.push(`${formatPct(netBuyersPct, 0)} net buyers`);
672
733
  lines.push(`${i + 1}. **${title}**${cidTag}` + (stats.length > 0 ? `\n ${stats.join(' · ')}` : ''));
673
734
  });
674
735
  lines.push('', `_$0.005 paid via x402._`);
@@ -681,34 +742,50 @@ async function execute(input, ctx) {
681
742
  isError: true,
682
743
  };
683
744
  }
684
- // Per-market drill-down. Official live registry:
685
- // /api/v1/pm/polymarket/market/:condition_id/smart-money
745
+ // Per-market drill-down. Requires a smart-wallet criterion (else 400).
746
+ // Predexon v2 returns a single `positioning` aggregate (net buyer/
747
+ // seller counts + smart buy/sell volume + avg prices), NOT buyers/
748
+ // sellers arrays — verified live 2026-05-20.
686
749
  const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
687
- const data = await getWithPayment(path, {}, ctx);
688
- const buyers = (data.buyers ?? []).slice(0, 5);
689
- const sellers = (data.sellers ?? []).slice(0, 5);
750
+ const data = await getWithPayment(path, {
751
+ min_total_pnl: DEFAULT_SMART_MIN_TOTAL_PNL,
752
+ }, ctx);
753
+ const pos = data.positioning;
754
+ if (!pos || typeof pos !== 'object') {
755
+ return { output: `No smart-money positioning recorded for \`${conditionId.slice(0, 14)}…\` yet.` };
756
+ }
690
757
  const lines = [
691
- `## Smart money — \`${conditionId.slice(0, 14)}…\``,
758
+ `## Smart money — ${pos.title ?? `\`${conditionId.slice(0, 14)}…\``}`,
692
759
  ];
693
- if (data.net_yes_size != null || data.net_no_size != null) {
694
- lines.push(`**Net flow:** YES ${formatUsd(data.net_yes_size)} / NO ${formatUsd(data.net_no_size)}`);
695
- }
696
- if (buyers.length > 0) {
697
- lines.push('', '**Top buyers**');
698
- buyers.forEach((b, i) => {
699
- const w = b.wallet ? `${b.wallet.slice(0, 8)}…${b.wallet.slice(-4)}` : 'unknown';
700
- lines.push(`${i + 1}. ${w} — ${formatUsd(b.size)} on ${b.outcome ?? 'unknown side'}`);
701
- });
702
- }
703
- if (sellers.length > 0) {
704
- lines.push('', '**Top sellers**');
705
- sellers.forEach((s, i) => {
706
- const w = s.wallet ? `${s.wallet.slice(0, 8)}…${s.wallet.slice(-4)}` : 'unknown';
707
- lines.push(`${i + 1}. ${w} — ${formatUsd(s.size)} on ${s.outcome ?? 'unknown side'}`);
708
- });
709
- }
710
- if (buyers.length === 0 && sellers.length === 0) {
711
- lines.push('No smart-money flow recorded for this market yet.');
760
+ const head = [];
761
+ if (pos.smart_wallet_count != null)
762
+ head.push(`${pos.smart_wallet_count} smart wallets`);
763
+ if (pos.net_buyers != null && pos.net_sellers != null)
764
+ head.push(`${pos.net_buyers} buyers / ${pos.net_sellers} sellers`);
765
+ if (pos.net_buyers_pct != null)
766
+ head.push(`${formatPct(pos.net_buyers_pct, 0)} net buyers`);
767
+ if (head.length > 0)
768
+ lines.push(head.join(' · '));
769
+ const flow = [];
770
+ if (pos.total_smart_buy_volume != null)
771
+ flow.push(`buy ${formatUsd(pos.total_smart_buy_volume)}`);
772
+ if (pos.total_smart_sell_volume != null)
773
+ flow.push(`sell ${formatUsd(pos.total_smart_sell_volume)}`);
774
+ if (pos.avg_smart_buy_price != null)
775
+ flow.push(`avg buy ${formatPct(pos.avg_smart_buy_price)}`);
776
+ if (pos.avg_smart_sell_price != null)
777
+ flow.push(`avg sell ${formatPct(pos.avg_smart_sell_price)}`);
778
+ if (flow.length > 0)
779
+ lines.push('', `**Smart flow:** ${flow.join(' · ')}`);
780
+ if (pos.total_smart_realized_pnl != null || pos.avg_smart_roi != null) {
781
+ const perf = [];
782
+ if (pos.total_smart_realized_pnl != null)
783
+ perf.push(`realized P&L ${formatUsd(pos.total_smart_realized_pnl)}`);
784
+ if (pos.avg_smart_roi != null)
785
+ perf.push(`avg ROI ${formatPct(pos.avg_smart_roi, 1)}`);
786
+ if (pos.avg_smart_win_rate != null)
787
+ perf.push(`win ${formatPct(pos.avg_smart_win_rate, 0)}`);
788
+ lines.push(`**Smart performance:** ${perf.join(' · ')}`);
712
789
  }
713
790
  lines.push('', `_$0.005 paid via x402._`);
714
791
  return { output: lines.join('\n') };
@@ -716,7 +793,7 @@ async function execute(input, ctx) {
716
793
  case 'searchPolymarket': {
717
794
  const raw = await getWithPayment('/v1/pm/polymarket/markets', {
718
795
  search,
719
- status: status ?? 'active',
796
+ status: normalizeMarketStatus(status) ?? 'open',
720
797
  sort: sort ?? 'volume',
721
798
  limit: cappedLimit,
722
799
  }, ctx);
@@ -732,13 +809,30 @@ async function execute(input, ctx) {
732
809
  '',
733
810
  ];
734
811
  markets.forEach((m, i) => {
735
- const yesPx = m.outcomes && m.outcome_prices && m.outcomes.length === m.outcome_prices.length
736
- ? m.outcomes.map((o, j) => `${o}=${formatPct(m.outcome_prices[j])}`).join(' / ')
737
- : 'n/a';
738
- const cid = m.condition_id ? ` · condition_id=\`${m.condition_id.slice(0, 14)}…\`` : '';
739
- lines.push(`${i + 1}. **${m.question || m.market_slug || 'untitled'}**${cid}\n` +
740
- ` prices: ${yesPx} · vol: ${formatUsd(m.volume)} · liq: ${formatUsd(m.liquidity)}` +
741
- (m.end_date ? ` · ends ${m.end_date.slice(0, 10)}` : ''));
812
+ // Prices: Predexon v2 nests each outcome as {label, price} inside
813
+ // `outcomes`. Fall back to the legacy parallel `outcomes[]` (strings)
814
+ // + `outcome_prices[]` shape if a gateway version still returns it.
815
+ let yesPx = 'n/a';
816
+ if (Array.isArray(m.outcomes) && m.outcomes.length > 0 && typeof m.outcomes[0] === 'object') {
817
+ const outs = m.outcomes;
818
+ const parts = outs
819
+ .filter(o => o && o.price != null)
820
+ .map(o => `${o.label ?? '?'}=${formatPct(o.price)}`);
821
+ if (parts.length > 0)
822
+ yesPx = parts.join(' / ');
823
+ }
824
+ else if (Array.isArray(m.outcomes) && Array.isArray(m.outcome_prices) && m.outcomes.length === m.outcome_prices.length) {
825
+ yesPx = m.outcomes.map((o, j) => `${o}=${formatPct(m.outcome_prices[j])}`).join(' / ');
826
+ }
827
+ const vol = m.total_volume_usd ?? m.volume;
828
+ const liq = m.liquidity_usd ?? m.liquidity;
829
+ const end = m.end_time ?? m.close_time ?? m.end_date;
830
+ // Full condition_id (NOT truncated) — the agent chains it into
831
+ // smartMoney; a truncated id 404s. Verified live 2026-05-20.
832
+ const cid = m.condition_id ? ` · condition_id=\`${m.condition_id}\`` : '';
833
+ lines.push(`${i + 1}. **${m.title || m.question || m.market_slug || 'untitled'}**${cid}\n` +
834
+ ` prices: ${yesPx} · vol: ${formatUsd(vol)} · liq: ${formatUsd(liq)}` +
835
+ (end ? ` · ends ${String(end).slice(0, 10)}` : ''));
742
836
  });
743
837
  lines.push('', `_$0.001 paid via x402._`);
744
838
  return { output: lines.join('\n') };
@@ -746,7 +840,7 @@ async function execute(input, ctx) {
746
840
  case 'searchKalshi': {
747
841
  const raw = await getWithPayment('/v1/pm/kalshi/markets', {
748
842
  search,
749
- status: status ?? 'open',
843
+ status: normalizeMarketStatus(status) ?? 'open',
750
844
  sort: sort ?? 'volume',
751
845
  limit: cappedLimit,
752
846
  }, ctx);
@@ -761,14 +855,20 @@ async function execute(input, ctx) {
761
855
  '',
762
856
  ];
763
857
  markets.forEach((m, i) => {
764
- // Kalshi quotes prices in cents (0–100). Surface them as a tight
765
- // bid/ask so the agent can read implied probability at a glance.
766
- const bid = m.yes_bid != null ? `${m.yes_bid}¢` : 'n/a';
767
- const ask = m.yes_ask != null ? `${m.yes_ask}¢` : 'n/a';
858
+ // Predexon v2 gives a single `last_price` (0–1 probability), not the
859
+ // yes_bid/yes_ask cents this once assumed. Render it as an implied
860
+ // YES %; fall back to legacy bid/ask if a gateway version sends them.
861
+ let yes = 'n/a';
862
+ if (m.last_price != null) {
863
+ yes = formatPct(m.last_price);
864
+ }
865
+ else if (m.yes_bid != null || m.yes_ask != null) {
866
+ yes = `${m.yes_bid ?? '?'}¢/${m.yes_ask ?? '?'}¢`;
867
+ }
768
868
  const ticker = m.ticker ? ` · ticker=\`${m.ticker}\`` : '';
769
869
  lines.push(`${i + 1}. **${m.title || m.ticker || 'untitled'}**${ticker}\n` +
770
- ` yes ${bid}/${ask} · vol: ${m.volume?.toLocaleString() ?? 'n/a'} · OI: ${m.open_interest?.toLocaleString() ?? 'n/a'}` +
771
- (m.close_time ? ` · closes ${m.close_time.slice(0, 10)}` : ''));
870
+ ` yes ${yes} · vol: ${m.volume?.toLocaleString() ?? 'n/a'} · OI: ${m.open_interest?.toLocaleString() ?? 'n/a'}` +
871
+ (m.close_time ? ` · closes ${String(m.close_time).slice(0, 10)}` : ''));
772
872
  });
773
873
  lines.push('', `_$0.001 paid via x402._`);
774
874
  return { output: lines.join('\n') };
@@ -787,11 +887,26 @@ async function execute(input, ctx) {
787
887
  '',
788
888
  ];
789
889
  pairs.forEach((p, i) => {
890
+ // Venues are nested UPPERCASE sub-objects in Predexon v2. pickString
891
+ // walks the sub-object's name keys (title/slug/...), so passing the
892
+ // whole `POLYMARKET` / `KALSHI` object resolves the title regardless
893
+ // of which name-bearing key is populated. Flat legacy fields are
894
+ // passed as fallbacks.
895
+ const poly = p.POLYMARKET ?? undefined;
896
+ const kalshi = p.KALSHI ?? undefined;
897
+ const polyTitle = pickString(poly, p.polymarket_question) ?? '(untitled)';
898
+ const kalshiTitle = pickString(kalshi, p.kalshi_title);
899
+ const ticker = (kalshi && kalshi.market_ticker) || p.kalshi_ticker;
790
900
  const sim = p.similarity != null ? ` · similarity ${formatPct(p.similarity, 0)}` : '';
791
- lines.push(`${i + 1}. **Polymarket:** ${p.polymarket_question || '(untitled)'}\n` +
792
- ` **Kalshi:** ${p.kalshi_title || '(untitled)'}` +
793
- (p.kalshi_ticker ? ` · ticker=\`${p.kalshi_ticker}\`` : '') +
794
- sim);
901
+ lines.push(`${i + 1}. **Polymarket:** ${polyTitle}`);
902
+ if (kalshi) {
903
+ lines.push(` **Kalshi:** ${kalshiTitle ?? '(untitled)'}` +
904
+ (ticker ? ` · ticker=\`${ticker}\`` : '') +
905
+ sim);
906
+ }
907
+ else {
908
+ lines.push(` _(no Kalshi match)_${sim}`);
909
+ }
795
910
  });
796
911
  lines.push('', `_$0.005 paid via x402._`);
797
912
  return { output: lines.join('\n') };
@@ -854,7 +969,7 @@ export const predictionMarketCapability = {
854
969
  },
855
970
  status: {
856
971
  type: 'string',
857
- description: 'Polymarket: active | closed | archived (default active). Kalshi: open | closed (default open). Forwarded to searchAll where supported.',
972
+ description: 'Market status filter Predexon accepts `open` or `closed` for Polymarket, Kalshi, and searchAll alike (default `open`). Synonyms like `active`/`archived` are normalized automatically.',
858
973
  },
859
974
  sort: {
860
975
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.21.6",
3
+ "version": "3.21.7",
4
4
  "description": "Franklin Agent — 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": {