bykaranteli-mcp 0.34.6 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  Notable changes to the `bykaranteli-mcp` package. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Dates are UTC and come from the git tag of each release (the version commit where no tag exists). The tool count is the number of tools the server registers in that release.
4
4
 
5
+ ## [0.35.0] - 2026-10-11
6
+
7
+ 67 tools.
8
+
9
+ ### Changed
10
+
11
+ - Short default answers. Twenty market tools answered between 48,000 and 234,000 characters on a call without arguments, and `get_venue_markets` answered nothing but the ceiling note. A call without the detail parameters now returns a first page: every shortened list is named in `trimmed` (`field`, `returned`, `total` and `more`, the parameter that returns the rest), and a shortened main list carries `rows_total` and `rows_returned`. Items are kept or left out as the route sent them; nothing is recomputed, field names stay, and `provenance` still describes the full route answer. Filtered calls (one symbol, coin, asset, pair, underlying or venue) keep their full detail.
12
+ - The result ceiling is 100,000 characters (was 240,000), under the 25,000 tokens Claude Code allows one tool result by default. `get_macro_liquidity` and `get_network_health` with a long `days` window and no `series` answer, when every series would pass it, with the series keys and the latest values instead of the generic ceiling note.
13
+ - Defaults per tool: `get_tradfi_gaps` 15 underlyings in the summary and the latest window; `get_borrow_rates` the last 24 hours of each series; `get_positioning` the 10 most traded perps and 24 points per CVD series; `get_macro_liquidity` and `get_network_health` the newest 7 points of each series; `get_tradfi_board`, `get_rsi_heatmap`, `get_solana_perps`, `get_hl_whales`, `get_settlements`, `get_withdrawal_status` and `get_leverage_tiers` 30 rows per list (`get_leverage_tiers` also the first 5 tiers of each ladder); `get_venue_markets` the 30 venue markets with the largest open interest; `get_liquidations` 30 rows without a symbol (summary still covers every matching row); `get_tokenized_stocks` 10 stocks (was 20) with their first 3 wrappers and 3 exchange pairs; `get_coverage` the first 30 snapshot feeds, English notes only and no matrix cells; `get_coinbase_premium` and `get_turkey_premium` the last 7 days of daily rows; `get_funding_arbitrage` the first 5 venue legs per row; `get_liqmap` the 30 real levels nearest the price and the 30 newest real prints; `get_lead_lag` the last 7 daily rows per pair.
14
+
15
+ ### Added
16
+
17
+ - Parameters that return the detail: `limit` on `get_liquidations`, `get_tradfi_gaps`, `get_positioning`, `get_tradfi_board`, `get_leverage_tiers`, `get_rsi_heatmap`, `get_settlements`, `get_hl_whales`, `get_solana_perps`, `get_withdrawal_status` and `get_venue_markets`; `asset` and `hours` on `get_borrow_rates`; `cvd_points` on `get_positioning`; `series` on `get_macro_liquidity` and `get_network_health` (one key over a long window); `order` (asc with `sort`, most oversold first) on `get_rsi_heatmap`; `base` on `get_settlements`; `symbol` on `get_funding_arbitrage`; `days` on `get_coinbase_premium` and `get_lead_lag`; `daily_days` on `get_turkey_premium`; `detail` on `get_coverage` and `get_liqmap`.
18
+
5
19
  ## [0.34.6] - 2026-10-09
6
20
 
7
21
  67 tools. README only; the server code is the same as 0.34.3.
package/README.md CHANGED
@@ -124,6 +124,8 @@ Requires Node.js 18 or newer.
124
124
 
125
125
  All market answers are JSON and carry a `generatedAt` timestamp, a `source` URL to the human-readable page and a `provenance` block: `source_page`, `api_path`, the answer's own `generated_at`, `fetched_at`, and when the answer carries them the `venues` behind the number, `coverage` (`full`, `sampled` or `mixed`), `stale_venues` and `recorded_since`, plus the `proof` page (<https://bykaranteli.com/proof>). A field the answer does not carry is left out, never filled in. Symbols accept both `BTCUSDT` and bare `BTC`.
126
126
 
127
+ Answers are sized for a model's context: a call without arguments returns a short first page (for example 30 rows, or the newest points of a series), never a whole board. Every shortened list is named in `trimmed` with `field`, `returned`, `total` and `more`, the parameter that returns the rest (`symbol`, `coin`, `asset`, `limit`, `days`, `detail` and the like); a filtered call keeps its full detail. One answer is capped at 100,000 characters; above that the tool says how to narrow the call.
128
+
127
129
  ## Account tools (alerts and watchlist)
128
130
 
129
131
  These tools read and change your own ByKaranteli account with the same key: the alert recipes that notify you on Telegram, email, push or webhook, and your watchlists. They never trade, move funds or touch another account.
package/dist/tools.js CHANGED
@@ -76,7 +76,7 @@ export function registerByKaranteliTools(server, options) {
76
76
  * context window, with no signal that anything was cut (audit 2026-09-04
77
77
  * #41). Above the ceiling the tool answers with guidance instead of a
78
78
  * payload the caller could not use anyway. */
79
- const MAX_RESULT_CHARS = 240_000;
79
+ const MAX_RESULT_CHARS = 100_000;
80
80
  function ok(data) {
81
81
  const text = JSON.stringify(data);
82
82
  if (text.length <= MAX_RESULT_CHARS)
@@ -93,6 +93,47 @@ export function registerByKaranteliTools(server, options) {
93
93
  }],
94
94
  };
95
95
  }
96
+ const isRec = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
97
+ const list = (v) => (Array.isArray(v) ? v : []);
98
+ class Trim {
99
+ cuts = [];
100
+ /** The first n items in the route's own order; anything that is not a longer list passes through. */
101
+ head(value, n, field, more) {
102
+ if (!Array.isArray(value) || value.length <= n)
103
+ return value;
104
+ this.cuts.push({ field, returned: n, total: value.length, more });
105
+ return value.slice(0, n);
106
+ }
107
+ /** One entry for a cut spread over several lists (a field inside every row). */
108
+ note(field, returned, total, more) {
109
+ if (returned < total)
110
+ this.cuts.push({ field, returned, total, more });
111
+ }
112
+ }
113
+ /* The answer with provenance already attached, shortened by `cut`; rows_total and rows_returned describe the main
114
+ * list when it is named (a route that already counts its rows keeps its own rows_total). */
115
+ function trimmedAnswer(full, cut, rowsField) {
116
+ if (!isRec(full))
117
+ return full;
118
+ const rec = { ...full };
119
+ const total = rowsField && Array.isArray(rec[rowsField]) ? rec[rowsField].length : undefined;
120
+ const t = new Trim();
121
+ cut(rec, t);
122
+ if (rowsField && total !== undefined && Array.isArray(rec[rowsField])) {
123
+ if (rec.rows_total === undefined)
124
+ rec.rows_total = total;
125
+ rec.rows_returned = rec[rowsField].length;
126
+ }
127
+ return t.cuts.length > 0 ? { ...rec, trimmed: t.cuts } : rec;
128
+ }
129
+ function compact(data, path, cut, rowsField) {
130
+ return trimmedAnswer(withProvenance(data, path), cut, rowsField);
131
+ }
132
+ /** The newest `days` distinct dates of rows carrying a `date` field, in the route's order. */
133
+ function newestDates(rows, days) {
134
+ const dates = [...new Set(rows.map((r) => (isRec(r) ? String(r.date) : "")))].filter(Boolean).sort().reverse();
135
+ return new Set(dates.slice(0, days));
136
+ }
96
137
  function fail(err) {
97
138
  const message = err instanceof Error ? err.message : String(err);
98
139
  const permanent = /returned HTTP 4\d\d/.test(message);
@@ -150,7 +191,7 @@ export function registerByKaranteliTools(server, options) {
150
191
  });
151
192
  server.registerTool("get_liquidations", {
152
193
  title: "Crypto liquidations: daily long/short totals per symbol and exchange",
153
- description: "Call this when the user asks how much was liquidated in crypto futures, whether longs or shorts got flushed, or for liquidation history. Returns daily long and short liquidation totals in USD per symbol and exchange, recorded from ByKaranteli's own stream collectors on every liquidation venue it counts, listed on bykaranteli.com/coverage (recorded events, a floor, not estimates). One row per finalized UTC day, symbol and exchange; history begins 2026-07-30 and grows daily.",
194
+ description: "Call this when the user asks how much was liquidated in crypto futures, whether longs or shorts got flushed, or for liquidation history. Returns daily long and short liquidation totals in USD per symbol and exchange, recorded from ByKaranteli's own stream collectors on every liquidation venue it counts, listed on bykaranteli.com/coverage (recorded events, a floor, not estimates). One row per finalized UTC day, symbol and exchange; history begins 2026-07-30 and grows daily. Without symbol, rows holds the first 30 rows (newest day, largest first) while summary covers every matching row; pass symbol or limit (up to 400) for more rows.",
154
195
  inputSchema: {
155
196
  symbol: z
156
197
  .string()
@@ -160,9 +201,10 @@ export function registerByKaranteliTools(server, options) {
160
201
  .optional()
161
202
  .describe("Optional symbol filter like BTCUSDT or ETHUSDT. Omit for all symbols."),
162
203
  days: z.number().int().min(1).max(90).optional().describe("How many most recent days to return (default 7)."),
204
+ limit: z.coerce.number().int().min(1).max(400).optional().describe("Optional. Rows to return, newest day first and largest first within a day (default 30 without symbol, 400 with one)."),
163
205
  },
164
206
  annotations: READ_ONLY,
165
- }, async ({ symbol, days }) => {
207
+ }, async ({ symbol, days, limit }) => {
166
208
  try {
167
209
  const d = (await fetchJson("/api/v1/public/datasets/liquidations-daily.json"));
168
210
  const rows = Array.isArray(d.rows) ? d.rows : [];
@@ -199,7 +241,8 @@ export function registerByKaranteliTools(server, options) {
199
241
  day.rows += 1;
200
242
  byDate.set(date, day);
201
243
  }
202
- const MAX_ROWS = 400;
244
+ /* 0.35.0: 30 rows without a symbol (the 400-row page was 78k characters); a symbol keeps the old 400. */
245
+ const MAX_ROWS = limit ?? (wantSymbol ? 400 : 30);
203
246
  /* Largest rows first within the cap, newest day first. */
204
247
  const ordered = [...filtered].sort((a, b) => {
205
248
  const dd = String(b.date).localeCompare(String(a.date));
@@ -227,14 +270,16 @@ export function registerByKaranteliTools(server, options) {
227
270
  })),
228
271
  },
229
272
  rows: ordered.slice(0, MAX_ROWS),
273
+ rows_total: filtered.length,
230
274
  rows_returned: Math.min(ordered.length, MAX_ROWS),
231
275
  rows_matched: filtered.length,
232
276
  truncated,
233
277
  note: truncated
234
- ? `rows is capped at ${MAX_ROWS} of ${filtered.length} matching rows (largest first). Use summary.totals and summary.by_date for complete figures, or pass a symbol filter. ` + baseNote
278
+ ? `rows is capped at ${MAX_ROWS} of ${filtered.length} matching rows (largest first). Use summary.totals and summary.by_date for complete figures, or pass a symbol filter or limit up to 400. ` + baseNote
235
279
  : baseNote,
236
280
  source: `${PUBLIC_URL}/liquidations`,
237
281
  provenance: provenanceFor(d, `/api/v1/public/datasets/liquidations-daily.json${wantSymbol ? `?symbol=${wantSymbol}` : ""}`, "/liquidations"),
282
+ ...(truncated ? { trimmed: [{ field: "rows", returned: MAX_ROWS, total: filtered.length, more: "pass symbol, or limit up to 400" }] } : {}),
238
283
  });
239
284
  }
240
285
  catch (err) {
@@ -312,13 +357,35 @@ export function registerByKaranteliTools(server, options) {
312
357
  });
313
358
  server.registerTool("get_funding_arbitrage", {
314
359
  title: "Cross-exchange funding arbitrage opportunities",
315
- description: "Call this when the user asks about funding arbitrage, funding rate differences between exchanges, or delta-neutral carry trades. Compares funding across every venue on the board, from Binance, OKX and Bybit to Hyperliquid, dYdX and the smaller perp venues fed by the venue snapshot, for 12 major perps and returns the best long/short venue per symbol with gross and net annualized APR (net of taker fees and weekly rebalance cost).",
316
- inputSchema: {},
360
+ description: "Call this when the user asks about funding arbitrage, funding rate differences between exchanges, or delta-neutral carry trades. Compares funding across every venue on the board, from Binance, OKX and Bybit to Hyperliquid, dYdX and the smaller perp venues fed by the venue snapshot, for 12 major perps and returns the best long/short venue per symbol with gross and net annualized APR (net of taker fees and weekly rebalance cost). Each row lists its first 5 venue legs; pass symbol for every leg of one perp.",
361
+ inputSchema: {
362
+ symbol: z.string().optional().describe("Optional. One perp, e.g. BTCUSDT or BTC: its row with every venue leg."),
363
+ },
317
364
  annotations: READ_ONLY,
318
- }, async () => {
365
+ }, async ({ symbol }) => {
319
366
  try {
320
367
  const d = (await fetchJson("/api/public/funding-arb"));
321
- return ok({ ...d, source: `${PUBLIC_URL}/funding-arb`, provenance: provenanceFor(d, "/api/public/funding-arb") });
368
+ const full = { ...d, source: `${PUBLIC_URL}/funding-arb`, provenance: provenanceFor(d, "/api/public/funding-arb") };
369
+ const rows = list(d.rows).filter(isRec);
370
+ if (symbol) {
371
+ const want = normalizeSymbol(symbol);
372
+ const row = rows.find((r) => r.symbol === want);
373
+ if (!row)
374
+ return ok({ generatedAt: d.generatedAt, note: `${want} is not on the funding arbitrage board. Symbols: ${rows.map((r) => String(r.symbol)).join(", ")}` });
375
+ return ok({ ...full, rows: [row] });
376
+ }
377
+ const LEGS = 5;
378
+ return ok(trimmedAnswer(full, (rec, t) => {
379
+ let kept = 0;
380
+ let total = 0;
381
+ rec.rows = rows.map((r) => {
382
+ const legs = list(r.exchanges);
383
+ total += legs.length;
384
+ kept += Math.min(legs.length, LEGS);
385
+ return legs.length > LEGS ? { ...r, exchanges: legs.slice(0, LEGS) } : r;
386
+ });
387
+ t.note("rows[].exchanges", kept, total, "pass symbol for every venue leg of one perp");
388
+ }));
322
389
  }
323
390
  catch (err) {
324
391
  return fail(err);
@@ -423,12 +490,22 @@ export function registerByKaranteliTools(server, options) {
423
490
  });
424
491
  server.registerTool("get_coinbase_premium", {
425
492
  title: "Coinbase Premium (US demand gauge) + carry yield",
426
- description: "Call this when the user asks whether US investors are buying or selling Bitcoin or Ethereum, about the Coinbase Premium, or what the cash-and-carry basis trade pays. Returns the latest daily premium in percent, 7-day average, same-sign streak, the last 30 days, and annualized quarterly carry yields. History since 2017; positive premium = US buying pressure.",
427
- inputSchema: {},
493
+ description: "Call this when the user asks whether US investors are buying or selling Bitcoin or Ethereum, about the Coinbase Premium, or what the cash-and-carry basis trade pays. Returns the latest daily premium in percent, 7-day average, same-sign streak, the last 30 days, and annualized quarterly carry yields. History since 2017; positive premium = US buying pressure. The kimchi and Turkey premium daily rows cover the last 7 days; pass days (up to 90) for more.",
494
+ inputSchema: {
495
+ days: z.coerce.number().int().min(1).max(90).optional().describe("Optional. Days of kimchi and Turkey premium daily rows, newest first (default 7, up to 90)."),
496
+ },
428
497
  annotations: READ_ONLY,
429
- }, async () => {
498
+ }, async ({ days }) => {
430
499
  try {
431
- return ok(withProvenance(await fetchJson("/api/public/premium"), "/api/public/premium"));
500
+ return ok(compact(await fetchJson("/api/public/premium"), "/api/public/premium", (rec, t) => {
501
+ if (!isRec(rec.kimchi))
502
+ return;
503
+ const daily = list(rec.kimchi.daily);
504
+ const keep = newestDates(daily, days ?? 7);
505
+ const kept = daily.filter((r) => isRec(r) && keep.has(String(r.date)));
506
+ t.note("kimchi.daily", kept.length, daily.length, "pass days up to 90");
507
+ rec.kimchi = { ...rec.kimchi, daily: kept };
508
+ }));
432
509
  }
433
510
  catch (err) {
434
511
  return fail(err);
@@ -551,7 +628,7 @@ export function registerByKaranteliTools(server, options) {
551
628
  });
552
629
  server.registerTool("get_tokenized_stocks", {
553
630
  title: "Tokenized stocks onchain: supply, premium to the real share, DEX pools and exchange pairs",
554
- description: "Call this when the user asks about tokenized stocks or stock tokens (xStocks, Ondo, Robinhood stock tokens, Coinbase tokenized stocks on Base, Binance bStocks, Backpack, Gate gStocks, Reality, Superstate, Figure, Dinari dShares, Securitize): how much of a stock exists onchain, which issuer or chain holds the most, whether a wrapper trades above or below the real share, which DEX pools or exchanges trade it. Returns the board ByKaranteli refreshes every 10 minutes: per underlying the reference share price and its session, each wrapper (issuer, chain, price, premium_pct against a fresh reference, DEX liquidity and 24h volume from admitted pools, supply in shares and dollars, holders, status), tokenized spot pairs on the exchanges the board lists, the perpetual futures cross, and totals (supply by issuer and chain, DEX volume, median premium). Each wrapper's price_source names the price that valued its supply dollars (reference: the share's reference price; issuer_oracle: the issuer's own price for the token; wrapper: its own pool or exchange price; null: no dollars), and totals.priced_by counts the counted wrappers per level, unpriced included. Wrappers come from issuer sources only, never from a name search.",
631
+ description: "Call this when the user asks about tokenized stocks or stock tokens (xStocks, Ondo, Robinhood stock tokens, Coinbase tokenized stocks on Base, Binance bStocks, Backpack, Gate gStocks, Reality, Superstate, Figure, Dinari dShares, Securitize): how much of a stock exists onchain, which issuer or chain holds the most, whether a wrapper trades above or below the real share, which DEX pools or exchanges trade it. Returns the board ByKaranteli refreshes every 10 minutes: per underlying the reference share price and its session, each wrapper (issuer, chain, price, premium_pct against a fresh reference, DEX liquidity and 24h volume from admitted pools, supply in shares and dollars, holders, status), tokenized spot pairs on the exchanges the board lists, the perpetual futures cross, and totals (supply by issuer and chain, DEX volume, median premium). Each wrapper's price_source names the price that valued its supply dollars (reference: the share's reference price; issuer_oracle: the issuer's own price for the token; wrapper: its own pool or exchange price; null: no dollars), and totals.priced_by counts the counted wrappers per level, unpriced included. Wrappers come from issuer sources only, never from a name search. Without underlying, each stock lists its first 3 wrappers and 3 exchange pairs; pass underlying for all of them.",
555
632
  inputSchema: {
556
633
  underlying: z.string().regex(/^[A-Za-z0-9.]{1,16}$/).optional().describe("Stock ticker, e.g. TSLA; omit for the whole board."),
557
634
  /* Issuer ids mirror TOKENIZED_ISSUER_IDS in web/src/lib/tokenized-equities/meta.ts (the hosted tool reads that list
@@ -561,7 +638,7 @@ export function registerByKaranteliTools(server, options) {
561
638
  .optional()
562
639
  .describe("One issuer: xstocks, robinhood, coinbase, bstocks, ondo, backpack, gstocks, reality, superstate, figure, dinari or securitize; omit for every issuer."),
563
640
  chain: z.string().regex(/^[a-z]{2,12}$/).optional().describe("One chain, e.g. solana, base, bnb, robinhood, ethereum, ton, all (xStocks circulating) or cex (exchange pairs)."),
564
- top: z.number().int().min(1).max(200).optional().describe("Rows by supply (default 20, max 200)."),
641
+ top: z.number().int().min(1).max(200).optional().describe("Rows by supply (default 10, max 200)."),
565
642
  },
566
643
  annotations: READ_ONLY,
567
644
  }, async ({ underlying, issuer, chain, top }) => {
@@ -573,9 +650,32 @@ export function registerByKaranteliTools(server, options) {
573
650
  params.set("issuer", issuer);
574
651
  if (chain)
575
652
  params.set("chain", chain);
576
- params.set("top", String(top ?? 20));
653
+ /* 0.35.0: 10 rows by default (20 full rows were 113k characters). */
654
+ params.set("top", String(top ?? 10));
577
655
  const path = `/api/public/tokenized-stocks?${params.toString()}`;
578
- return ok(withProvenance(await fetchJson(path), path));
656
+ const data = await fetchJson(path);
657
+ if (underlying)
658
+ return ok(withProvenance(data, path));
659
+ const N = 3;
660
+ return ok(compact(data, path, (rec, t) => {
661
+ const kept = { wrappers: 0, cex: 0 };
662
+ const total = { wrappers: 0, cex: 0 };
663
+ rec.rows = list(rec.rows).map((r) => {
664
+ if (!isRec(r))
665
+ return r;
666
+ const out = { ...r };
667
+ for (const key of ["wrappers", "cex"]) {
668
+ const items = list(r[key]);
669
+ total[key] += items.length;
670
+ kept[key] += Math.min(items.length, N);
671
+ if (items.length > N)
672
+ out[key] = items.slice(0, N);
673
+ }
674
+ return out;
675
+ });
676
+ t.note("rows[].wrappers", kept.wrappers, total.wrappers, "pass underlying for every wrapper of one stock");
677
+ t.note("rows[].cex", kept.cex, total.cex, "pass underlying for every exchange pair of one stock");
678
+ }, "rows"));
579
679
  }
580
680
  catch (err) {
581
681
  return fail(err);
@@ -680,12 +780,42 @@ export function registerByKaranteliTools(server, options) {
680
780
  });
681
781
  server.registerTool("get_borrow_rates", {
682
782
  title: "Margin borrow rates per venue: the cost of leverage, hourly",
683
- description: "Call this when the user asks what it costs to borrow USDT, USDC, BTC, ETH or a major alt on an exchange, which venue has the cheapest borrow, whether stablecoin borrow cost is spiking, or what the carry of a basis trade is on a venue (funding minus borrow). Returns the latest annualised rate per venue and asset, 30 days of hourly series for the stablecoins and majors, and the carry table. Recorded hourly by ByKaranteli (Binance and OKX today).",
684
- inputSchema: {},
783
+ description: "Call this when the user asks what it costs to borrow USDT, USDC, BTC, ETH or a major alt on an exchange, which venue has the cheapest borrow, whether stablecoin borrow cost is spiking, or what the carry of a basis trade is on a venue (funding minus borrow). Returns the latest annualised rate per venue and asset, 30 days of hourly series for the stablecoins and majors, and the carry table. Recorded hourly by ByKaranteli (Binance and OKX today). The series hold the last 24 hours unless hours (up to 720) or asset is passed.",
784
+ inputSchema: {
785
+ asset: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,12}$/).optional().describe("Optional. One asset, e.g. USDT: its rates, carry and full 30-day series."),
786
+ hours: z.coerce.number().int().min(1).max(720).optional().describe("Optional. Hours of hourly series to return (default 24, or the full 720 with asset)."),
787
+ },
685
788
  annotations: READ_ONLY,
686
- }, async () => {
789
+ }, async ({ asset, hours }) => {
687
790
  try {
688
- return ok(withProvenance(await fetchJson("/api/public/borrow-rates"), "/api/public/borrow-rates"));
791
+ const path = "/api/public/borrow-rates";
792
+ return ok(compact(await fetchJson(path), path, (rec, t) => {
793
+ const byAsset = (r) => !asset || (isRec(r) && String(r.asset).toUpperCase() === asset);
794
+ if (asset) {
795
+ rec.latest = list(rec.latest).filter(byAsset);
796
+ rec.carry = list(rec.carry).filter(byAsset);
797
+ if (list(rec.latest).length === 0)
798
+ rec.note = `${asset} has no recorded borrow rate; omit asset for every recorded one.`;
799
+ }
800
+ const series = list(rec.series).filter(byAsset).filter(isRec);
801
+ /* Hourly points [ts_ms, value], oldest first; the window ends at the newest point of any series. */
802
+ let newest = 0;
803
+ for (const s of series)
804
+ for (const p of list(s.points))
805
+ if (Array.isArray(p) && Number(p[0]) > newest)
806
+ newest = Number(p[0]);
807
+ const cutoff = newest - (hours ?? (asset ? 720 : 24)) * 3_600_000;
808
+ let kept = 0;
809
+ let total = 0;
810
+ rec.series = series.map((s) => {
811
+ const points = list(s.points);
812
+ const inWindow = points.filter((p) => Array.isArray(p) && Number(p[0]) > cutoff);
813
+ total += points.length;
814
+ kept += inWindow.length;
815
+ return { ...s, points: inWindow };
816
+ });
817
+ t.note("series[].points", kept, total, "pass hours up to 720, or asset for one asset's full series");
818
+ }));
689
819
  }
690
820
  catch (err) {
691
821
  return fail(err);
@@ -706,12 +836,26 @@ export function registerByKaranteliTools(server, options) {
706
836
  });
707
837
  server.registerTool("get_settlements", {
708
838
  title: "Expiry calendar and settlement prices across venues",
709
- description: "Call this when the user asks what futures or options expire soon, when the next quarterly expiry is on an exchange, how many contracts settle this week, or at what price a dated future settled. Returns the next 60 days of dated future and option expiries grouped by date, venue and underlying from 54 venues' market lists, plus the settlement prices recorded as dated futures deliver.",
710
- inputSchema: {},
839
+ description: "Call this when the user asks what futures or options expire soon, when the next quarterly expiry is on an exchange, how many contracts settle this week, or at what price a dated future settled. Returns the next 60 days of dated future and option expiries grouped by date, venue and underlying from 54 venues' market lists, plus the settlement prices recorded as dated futures deliver. Without base, the first 30 expiry rows and the 30 newest settlement prices; pass base for one asset's full lists, or limit (up to 300).",
840
+ inputSchema: {
841
+ base: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{1,16}$/).optional().describe("Optional. One underlying, e.g. BTC: every expiry row and settlement price of that asset."),
842
+ limit: z.coerce.number().int().min(1).max(300).optional().describe("Optional. Rows per list without base (default 30): expiries soonest first, settlements newest first."),
843
+ },
711
844
  annotations: READ_ONLY,
712
- }, async () => {
845
+ }, async ({ base, limit }) => {
713
846
  try {
714
- return ok(withProvenance(await fetchJson("/api/public/settlements"), "/api/public/settlements"));
847
+ const path = "/api/public/settlements";
848
+ return ok(compact(await fetchJson(path), path, (rec, t) => {
849
+ if (base) {
850
+ const byBase = (r) => isRec(r) && String(r.base).toUpperCase() === base;
851
+ rec.upcoming = list(rec.upcoming).filter(byBase);
852
+ rec.recorded = list(rec.recorded).filter(byBase);
853
+ return;
854
+ }
855
+ const n = limit ?? 30;
856
+ rec.upcoming = t.head(rec.upcoming, n, "upcoming", "pass base for one asset, or limit up to 300");
857
+ rec.recorded = t.head(rec.recorded, n, "recorded", "pass base for one asset, or limit up to 300");
858
+ }, base ? undefined : "upcoming"));
715
859
  }
716
860
  catch (err) {
717
861
  return fail(err);
@@ -732,10 +876,11 @@ export function registerByKaranteliTools(server, options) {
732
876
  });
733
877
  server.registerTool("get_withdrawal_status", {
734
878
  title: "Withdrawal status and network fees: which exchanges paused withdrawals, per asset and network",
735
- description: "Call this when the user asks whether an exchange has paused withdrawals or deposits, which networks are open for an asset, what the withdrawal fee or minimum is on each venue, or which venue is cheapest to withdraw from. Without arguments returns the overview (withdrawals paused right now, ranked, plus recent suspension and resumption events). Pass asset (e.g. USDT) for every venue and network of that asset, and venue (e.g. kucoin) to narrow. Recorded daily by ByKaranteli from 20+ venues' public currency lists.",
879
+ description: "Call this when the user asks whether an exchange has paused withdrawals or deposits, which networks are open for an asset, what the withdrawal fee or minimum is on each venue, or which venue is cheapest to withdraw from. Without arguments returns the overview (withdrawals paused right now, ranked, plus recent suspension and resumption events). Pass asset (e.g. USDT) for every venue and network of that asset, and venue (e.g. kucoin) to narrow. Recorded daily by ByKaranteli from 20+ venues' public currency lists. The overview lists the first 30 paused networks and the 30 newest events; pass asset, or limit (up to 400) for more.",
736
880
  inputSchema: {
737
881
  asset: z.string().optional().describe("string, optional asset code, e.g. USDT"),
738
882
  venue: z.string().optional().describe("string, optional venue id, e.g. kucoin"),
883
+ limit: z.coerce.number().int().min(1).max(400).optional().describe("Optional. Rows of the overview lists (paused networks, events) without asset (default 30)."),
739
884
  },
740
885
  annotations: READ_ONLY,
741
886
  }, async (args) => {
@@ -746,7 +891,15 @@ export function registerByKaranteliTools(server, options) {
746
891
  if (args.venue)
747
892
  qs.set("venue", String(args.venue).toLowerCase());
748
893
  const q = qs.toString();
749
- return ok(withProvenance(await fetchJson(`/api/public/withdrawals${q ? `?${q}` : ""}`), `/api/public/withdrawals${q ? `?${q}` : ""}`));
894
+ const path = `/api/public/withdrawals${q ? `?${q}` : ""}`;
895
+ const data = await fetchJson(path);
896
+ if (args.asset)
897
+ return ok(withProvenance(data, path));
898
+ const n = args.limit ?? 30;
899
+ return ok(compact(data, path, (rec, t) => {
900
+ rec.suspended_rows = t.head(rec.suspended_rows, n, "suspended_rows", "pass asset for every venue and network of one asset, or limit up to 400");
901
+ rec.events = t.head(rec.events, n, "events", "pass limit up to 400");
902
+ }, "suspended_rows"));
750
903
  }
751
904
  catch (err) {
752
905
  return fail(err);
@@ -754,10 +907,11 @@ export function registerByKaranteliTools(server, options) {
754
907
  });
755
908
  server.registerTool("get_leverage_tiers", {
756
909
  title: "Leverage tiers: max leverage and maintenance margin per perpetual on every venue",
757
- description: "Call this when the user asks how much leverage an exchange allows on a coin, what the maintenance margin or risk limit ladder is, which venue offers the highest leverage for a symbol, or whether an exchange recently cut leverage. Returns the current ladder per venue (tier, notional floor and cap, max leverage, maintenance margin rate) recorded daily by ByKaranteli, plus a change log. Pass symbol for one base asset (e.g. SOL) and venue for one exchange (bybit, okx, gate, htx, bitget, mexc).",
910
+ description: "Call this when the user asks how much leverage an exchange allows on a coin, what the maintenance margin or risk limit ladder is, which venue offers the highest leverage for a symbol, or whether an exchange recently cut leverage. Returns the current ladder per venue (tier, notional floor and cap, max leverage, maintenance margin rate) recorded daily by ByKaranteli, plus a change log. Pass symbol for one base asset (e.g. SOL) and venue for one exchange (bybit, okx, gate, htx, bitget, mexc). Without symbol, the first 30 rows of each list and the first 5 tiers of each ladder; pass symbol for every tier, or limit (up to 300).",
758
911
  inputSchema: {
759
912
  symbol: z.string().optional().describe("string, optional base asset, e.g. BTC"),
760
913
  venue: z.string().optional().describe("string, optional venue id, e.g. bybit"),
914
+ limit: z.coerce.number().int().min(1).max(300).optional().describe("Optional. Rows per list without symbol (default 30)."),
761
915
  },
762
916
  annotations: READ_ONLY,
763
917
  }, async (args) => {
@@ -768,7 +922,36 @@ export function registerByKaranteliTools(server, options) {
768
922
  if (args.venue)
769
923
  qs.set("venue", String(args.venue).toLowerCase());
770
924
  const q = qs.toString();
771
- return ok(withProvenance(await fetchJson(`/api/public/leverage-tiers${q ? `?${q}` : ""}`), `/api/public/leverage-tiers${q ? `?${q}` : ""}`));
925
+ const path = `/api/public/leverage-tiers${q ? `?${q}` : ""}`;
926
+ const data = await fetchJson(path);
927
+ if (args.symbol)
928
+ return ok(withProvenance(data, path));
929
+ const n = args.limit ?? 30;
930
+ const more = "pass symbol for every tier of one base asset, or limit up to 300";
931
+ return ok(compact(data, path, (rec, t) => {
932
+ /* venue alone: one flat list of every tier of every contract on that venue (2,000 rows on Bybit). */
933
+ rec.rows = t.head(rec.rows, n, "rows", more);
934
+ rec.compare = t.head(rec.compare, n, "compare", more);
935
+ rec.changes = t.head(rec.changes, n, "changes", "pass limit up to 300");
936
+ if (isRec(rec.ladders)) {
937
+ const TIERS = 5;
938
+ let kept = 0;
939
+ let total = 0;
940
+ const ladders = {};
941
+ for (const [base, venues] of Object.entries(rec.ladders)) {
942
+ ladders[base] = list(venues).map((v) => {
943
+ if (!isRec(v))
944
+ return v;
945
+ const rows = list(v.rows);
946
+ total += rows.length;
947
+ kept += Math.min(rows.length, TIERS);
948
+ return rows.length > TIERS ? { ...v, rows: rows.slice(0, TIERS) } : v;
949
+ });
950
+ }
951
+ rec.ladders = ladders;
952
+ t.note("ladders.*[].rows", kept, total, "pass symbol for every tier of one base asset");
953
+ }
954
+ }, isRec(data) && Array.isArray(data.rows) ? "rows" : "compare"));
772
955
  }
773
956
  catch (err) {
774
957
  return fail(err);
@@ -776,10 +959,11 @@ export function registerByKaranteliTools(server, options) {
776
959
  });
777
960
  server.registerTool("get_venue_markets", {
778
961
  title: "Exchange coverage: OI, volume, funding and pegs across every exchange we snapshot",
779
- description: "Call this when the user asks about total open interest across exchanges, which venues hold the most OI, DEX versus CEX share, funding dispersion between venues, or stablecoin pegs. Returns the latest 10-minute snapshot aggregates across every perpetual and spot feed we poll (the coverage field lists them); pass symbol for one coin's per-venue rows.",
962
+ description: "Call this when the user asks about total open interest across exchanges, which venues hold the most OI, DEX versus CEX share, funding dispersion between venues, or stablecoin pegs. Returns the latest 10-minute snapshot aggregates across every perpetual and spot feed we poll (the coverage field lists them); pass symbol for one coin's per-venue rows. Without symbol, rows holds the 30 venue markets with the largest open interest; pass symbol, or limit (up to 250) for more.",
780
963
  inputSchema: {
781
964
  symbol: z.string().optional().describe("string, optional base asset, e.g. BTC"),
782
965
  history_days: z.number().int().min(1).max(90).optional().describe("Return the hourly multi-venue open interest history (total, DEX share, OI-weighted funding) for this many days instead of the snapshot"),
966
+ limit: z.coerce.number().int().min(1).max(250).optional().describe("Optional. Venue market rows without symbol, largest open interest first (default 30)."),
783
967
  },
784
968
  annotations: READ_ONLY,
785
969
  }, async (args) => {
@@ -787,7 +971,17 @@ export function registerByKaranteliTools(server, options) {
787
971
  const path = args.history_days
788
972
  ? `/api/public/venues/oi-history?days=${Math.min(90, Math.floor(args.history_days))}`
789
973
  : args.symbol ? `/api/public/venues/markets?symbol=${encodeURIComponent(args.symbol.toUpperCase())}` : "/api/public/venues/markets";
790
- return ok(withProvenance(await fetchJson(path), path));
974
+ const data = await fetchJson(path);
975
+ if (args.history_days || args.symbol)
976
+ return ok(withProvenance(data, path));
977
+ /* The route lists rows by venue; the board's own ranking is open interest, then 24h volume for the rows
978
+ * without open interest (spot). */
979
+ const n = Number(args.limit ?? 30);
980
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : -1);
981
+ return ok(compact(data, path, (rec, t) => {
982
+ const ranked = list(rec.rows).filter(isRec).sort((a, b) => num(b.open_interest_usd) - num(a.open_interest_usd) || num(b.volume_24h_usd) - num(a.volume_24h_usd));
983
+ rec.rows = t.head(ranked, n, "rows", "pass symbol for one coin's rows on every venue, or limit up to 250");
984
+ }, "rows"));
791
985
  }
792
986
  catch (err) {
793
987
  return fail(err);
@@ -795,12 +989,28 @@ export function registerByKaranteliTools(server, options) {
795
989
  });
796
990
  server.registerTool("get_lead_lag", {
797
991
  title: "Venue lead-lag: who moves first (Coinbase, Kraken, Binance)",
798
- description: "Call this when the user asks which exchange leads price discovery or whether spot or perp moves first. Returns per-pair daily cross-correlations of one-minute returns at lags -3..+3 and the lead asymmetry, with the share of days each venue led.",
799
- inputSchema: {},
992
+ description: "Call this when the user asks which exchange leads price discovery or whether spot or perp moves first. Returns per-pair daily cross-correlations of one-minute returns at lags -3..+3 and the lead asymmetry, with the share of days each venue led. Each pair's daily rows cover the last 7 days (its summary covers the whole window); pass days (up to 30) for more.",
993
+ inputSchema: {
994
+ days: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Daily rows per pair, newest kept (default 7, up to 30)."),
995
+ },
800
996
  annotations: READ_ONLY,
801
- }, async () => {
997
+ }, async ({ days }) => {
802
998
  try {
803
- return ok(withProvenance(await fetchJson("/api/public/venues/lead-lag"), "/api/public/venues/lead-lag"));
999
+ const path = "/api/public/venues/lead-lag";
1000
+ return ok(compact(await fetchJson(path), path, (rec, t) => {
1001
+ const n = days ?? 7;
1002
+ let kept = 0;
1003
+ let total = 0;
1004
+ rec.pairs = list(rec.pairs).map((p) => {
1005
+ if (!isRec(p) || !Array.isArray(p.days))
1006
+ return p;
1007
+ /* Oldest day first, so the newest n are the tail. */
1008
+ total += p.days.length;
1009
+ kept += Math.min(p.days.length, n);
1010
+ return p.days.length > n ? { ...p, days: p.days.slice(-n) } : p;
1011
+ });
1012
+ t.note("pairs[].days", kept, total, "pass days up to 30");
1013
+ }));
804
1014
  }
805
1015
  catch (err) {
806
1016
  return fail(err);
@@ -860,15 +1070,65 @@ export function registerByKaranteliTools(server, options) {
860
1070
  return fail(err);
861
1071
  }
862
1072
  });
1073
+ /* Daily series boards (macro, network): a call without days reads the route's shortest window (30 days) and keeps the
1074
+ * newest 7 points of each series; a call with days gets the whole window it asked for. */
1075
+ async function dailySeries(route, days, key) {
1076
+ const wanted = days && days > 0 ? Math.floor(days) : undefined;
1077
+ const path = `${route}?days=${wanted ?? 30}`;
1078
+ const data = await fetchJson(path);
1079
+ /* series narrows the answer to one key, so a long window of one series fits where every series would not. */
1080
+ const narrowed = key && isRec(data) && isRec(data.series)
1081
+ ? (() => {
1082
+ if (!Object.hasOwn(data.series, key))
1083
+ throw new InputError(`series must be one of ${Object.keys(data.series).join(", ")}.`);
1084
+ return { ...data, series: { [key]: data.series[key] } };
1085
+ })()
1086
+ : data;
1087
+ if (wanted) {
1088
+ const answer = withProvenance(narrowed, path);
1089
+ /* Every series over a long window can pass the ceiling (macro, 365 days: 146k characters); name the keys that
1090
+ * fit instead of the generic ceiling note. */
1091
+ if (!key && isRec(data) && isRec(data.series) && JSON.stringify(answer).length > MAX_RESULT_CHARS) {
1092
+ const keys = Object.keys(data.series);
1093
+ return ok({
1094
+ truncated: true,
1095
+ limit: MAX_RESULT_CHARS,
1096
+ note: `Every series over ${wanted} days does not fit one answer. Pass series (one of series_keys) for one key over this window, or fewer days for all of them.`,
1097
+ series_keys: keys,
1098
+ ...(isRec(data.latest) ? { latest: data.latest } : {}),
1099
+ });
1100
+ }
1101
+ return ok(answer);
1102
+ }
1103
+ return ok(compact(narrowed, path, (rec, t) => {
1104
+ if (!isRec(rec.series))
1105
+ return;
1106
+ const POINTS = 7;
1107
+ let kept = 0;
1108
+ let total = 0;
1109
+ const series = {};
1110
+ for (const [key, points] of Object.entries(rec.series)) {
1111
+ const all = list(points);
1112
+ total += all.length;
1113
+ kept += Math.min(all.length, POINTS);
1114
+ /* Oldest date first, so the newest points are the tail. */
1115
+ series[key] = all.length > POINTS ? all.slice(-POINTS) : points;
1116
+ }
1117
+ rec.series = series;
1118
+ t.note("series.*", kept, total, "pass days (30 to 4000) for the daily series, and series for one key over a long window");
1119
+ }));
1120
+ }
863
1121
  server.registerTool("get_macro_liquidity", {
864
1122
  title: "Macro liquidity: Fed funds, 10y, balance sheet, RRP, stablecoin supply",
865
- description: "Call this when the user asks about macro liquidity, the Fed balance sheet, reverse repo, rates or stablecoin supply in relation to crypto. Returns the recorded daily series and latest values.",
866
- inputSchema: { days: z.number().int().min(30).max(4000).optional().describe("Window in days, 30-4000 (default 365); the route clamps to that range.") },
1123
+ description: "Call this when the user asks about macro liquidity, the Fed balance sheet, reverse repo, rates or stablecoin supply in relation to crypto. Returns the recorded daily series and latest values. Without days, each series holds its newest 7 points; pass days for the daily series, and series for one key over a long window.",
1124
+ inputSchema: {
1125
+ days: z.number().int().min(30).max(4000).optional().describe("Window in days, 30-4000; the route clamps to that range. Omit for the newest 7 points of each series."),
1126
+ series: z.string().trim().regex(/^[a-z0-9_]{2,60}$/).optional().describe("Optional. One series key from the answer's series field, e.g. fred_walcl_busd."),
1127
+ },
867
1128
  annotations: READ_ONLY,
868
1129
  }, async (args) => {
869
1130
  try {
870
- const path = `/api/public/macro?days=${args.days && args.days > 0 ? Math.floor(args.days) : 365}`;
871
- return ok(withProvenance(await fetchJson(path), path));
1131
+ return await dailySeries("/api/public/macro", args.days, args.series);
872
1132
  }
873
1133
  catch (err) {
874
1134
  return fail(err);
@@ -876,13 +1136,15 @@ export function registerByKaranteliTools(server, options) {
876
1136
  });
877
1137
  server.registerTool("get_network_health", {
878
1138
  title: "Bitcoin network health from our own node",
879
- description: "Call this when the user asks about Bitcoin hashrate, difficulty or block fees (our node runs blocksonly, so there is no mempool series). Returns the recorded daily series and latest values measured on ByKaranteli's own node.",
880
- inputSchema: { days: z.number().int().min(30).max(4000).optional().describe("Window in days, 30-4000 (default 365); the route clamps to that range.") },
1139
+ description: "Call this when the user asks about Bitcoin hashrate, difficulty or block fees (our node runs blocksonly, so there is no mempool series). Returns the recorded daily series and latest values measured on ByKaranteli's own node. Without days, each series holds its newest 7 points; pass days for the daily series, and series for one key over a long window.",
1140
+ inputSchema: {
1141
+ days: z.number().int().min(30).max(4000).optional().describe("Window in days, 30-4000; the route clamps to that range. Omit for the newest 7 points of each series."),
1142
+ series: z.string().trim().regex(/^[a-z0-9_]{2,60}$/).optional().describe("Optional. One series key from the answer's series field, e.g. btc_hashrate_ehs."),
1143
+ },
881
1144
  annotations: READ_ONLY,
882
1145
  }, async (args) => {
883
1146
  try {
884
- const path = `/api/public/network?days=${args.days && args.days > 0 ? Math.floor(args.days) : 365}`;
885
- return ok(withProvenance(await fetchJson(path), path));
1147
+ return await dailySeries("/api/public/network", args.days, args.series);
886
1148
  }
887
1149
  catch (err) {
888
1150
  return fail(err);
@@ -890,7 +1152,7 @@ export function registerByKaranteliTools(server, options) {
890
1152
  });
891
1153
  server.registerTool("get_liqmap", {
892
1154
  title: "LiqMap: estimated liquidation clusters with real prints overlaid",
893
- description: "Call this when the user asks where liquidation clusters or liquidity pools sit for a perpetual, where leveraged longs/shorts would get liquidated, or for a liquidation heatmap reading. Returns the LiqMap snapshot for one symbol: modeled liquidation levels by price, zone aggregates and real liquidation prints from every liquidation venue ByKaranteli counts (listed on bykaranteli.com/coverage). Without an account key (or on the Free plan) the 24h view; with a Builder or higher key (BYKARANTELI_API_KEY) every timeframe from 1h to 30d.",
1155
+ description: "Call this when the user asks where liquidation clusters or liquidity pools sit for a perpetual, where leveraged longs/shorts would get liquidated, or for a liquidation heatmap reading. Returns the LiqMap snapshot for one symbol: modeled liquidation levels by price, zone aggregates and real liquidation prints from every liquidation venue ByKaranteli counts (listed on bykaranteli.com/coverage). Without an account key (or on the Free plan) the 24h view; with a Builder or higher key (BYKARANTELI_API_KEY) every timeframe from 1h to 30d. By default the 30 real levels nearest the price and the 30 newest real prints; pass detail=true for all of them.",
894
1156
  inputSchema: {
895
1157
  symbol: z
896
1158
  .string()
@@ -903,16 +1165,34 @@ export function registerByKaranteliTools(server, options) {
903
1165
  .enum(["1h", "4h", "12h", "24h", "3d", "1w", "30d"])
904
1166
  .optional()
905
1167
  .describe("Model window. Default 24h, the only one served without a Builder or higher key."),
1168
+ detail: z.boolean().optional().describe("Optional. true: every real level and every real print (default false: the 30 nearest levels, the 30 newest prints)."),
906
1169
  },
907
1170
  annotations: READ_ONLY,
908
- }, async ({ symbol, timeframe }) => {
1171
+ }, async ({ symbol, timeframe, detail }) => {
909
1172
  try {
910
1173
  const raw = symbol ?? "BTCUSDT";
911
1174
  const sym = raw.endsWith("USDT") ? raw : `${raw}USDT`;
912
1175
  const path = `/api/liqmap/public?symbol=${encodeURIComponent(sym)}&timeframe=${encodeURIComponent(timeframe ?? "24h")}`;
913
1176
  const data = await fetchJson(path);
914
1177
  const page = `/liqmap/${sym.replace(/USDT$/, "").toLowerCase()}`;
915
- return ok({ ...data, source: `${PUBLIC_URL}${page}`, provenance: provenanceFor(data, path, page) });
1178
+ const full = { ...data, source: `${PUBLIC_URL}${page}`, provenance: provenanceFor(data, path, page) };
1179
+ if (detail)
1180
+ return ok(full);
1181
+ const N = 30;
1182
+ return ok(trimmedAnswer(full, (rec, t) => {
1183
+ if (isRec(rec.real_levels) && Array.isArray(rec.real_levels.levels) && rec.real_levels.levels.length > N) {
1184
+ /* The levels nearest the price, kept in the route's price order. */
1185
+ const levels = rec.real_levels.levels;
1186
+ const dist = (l) => (isRec(l) && typeof l.distance_pct === "number" ? Math.abs(l.distance_pct) : Infinity);
1187
+ const nearest = new Set([...levels].sort((a, b) => dist(a) - dist(b)).slice(0, N));
1188
+ rec.real_levels = { ...rec.real_levels, levels: levels.filter((l) => nearest.has(l)) };
1189
+ t.note("real_levels.levels", N, levels.length, "pass detail=true for every level");
1190
+ }
1191
+ if (isRec(rec.real_liquidations)) {
1192
+ /* Newest print first. */
1193
+ rec.real_liquidations = { ...rec.real_liquidations, points: t.head(rec.real_liquidations.points, N, "real_liquidations.points", "pass detail=true for every print") };
1194
+ }
1195
+ }));
916
1196
  }
917
1197
  catch (err) {
918
1198
  return fail(err);
@@ -920,7 +1200,7 @@ export function registerByKaranteliTools(server, options) {
920
1200
  });
921
1201
  server.registerTool("get_tradfi_board", {
922
1202
  title: "TradFi perpetuals: stock, index and commodity perps on Binance",
923
- description: "Call this when the user asks about stock perpetuals (TSLA, NVDA, AAPL, gold, S&P 500...), tokenized-equity perps, TradFi perp funding rates, open interest, liquidations, which exchanges list a stock perp, or whether the equity session is open. Returns Binance's TradFi perpetual board: per contract mark, index, basis, funding, 24h change and volume, open interest, 24h recorded liquidations, other venues listing the same underlying, and the trading-session state per market. Filter by market (EQUITY, HK_EQUITY, KR_EQUITY, CN_EQUITY, COMMODITY, INDEX, PREMARKET) or one symbol.",
1203
+ description: "Call this when the user asks about stock perpetuals (TSLA, NVDA, AAPL, gold, S&P 500...), tokenized-equity perps, TradFi perp funding rates, open interest, liquidations, which exchanges list a stock perp, or whether the equity session is open. Returns Binance's TradFi perpetual board: per contract mark, index, basis, funding, 24h change and volume, open interest, 24h recorded liquidations, other venues listing the same underlying, and the trading-session state per market. Filter by market (EQUITY, HK_EQUITY, KR_EQUITY, CN_EQUITY, COMMODITY, INDEX, PREMARKET) or one symbol. Without symbol, the first 30 contracts by open interest; pass symbol, or limit (up to 150) for more.",
924
1204
  inputSchema: {
925
1205
  market: z
926
1206
  .string()
@@ -936,9 +1216,10 @@ export function registerByKaranteliTools(server, options) {
936
1216
  .regex(/^[A-Z0-9]{2,24}$/)
937
1217
  .optional()
938
1218
  .describe("One Binance TradFi symbol, e.g. TSLAUSDT"),
1219
+ limit: z.coerce.number().int().min(1).max(150).optional().describe("Optional. Contracts without symbol, largest open interest first (default 30)."),
939
1220
  },
940
1221
  annotations: READ_ONLY,
941
- }, async ({ market, symbol }) => {
1222
+ }, async ({ market, symbol, limit }) => {
942
1223
  try {
943
1224
  const params = new URLSearchParams();
944
1225
  if (market)
@@ -947,7 +1228,12 @@ export function registerByKaranteliTools(server, options) {
947
1228
  params.set("symbol", symbol);
948
1229
  const qs = params.toString();
949
1230
  const path = `/api/public/tradfi${qs ? `?${qs}` : ""}`;
950
- return ok(withProvenance(await fetchJson(path), path));
1231
+ const data = await fetchJson(path);
1232
+ if (symbol)
1233
+ return ok(withProvenance(data, path));
1234
+ return ok(compact(data, path, (rec, t) => {
1235
+ rec.rows = t.head(rec.rows, limit ?? 30, "rows", "pass symbol, market, or limit up to 150");
1236
+ }, "rows"));
951
1237
  }
952
1238
  catch (err) {
953
1239
  return fail(err);
@@ -955,14 +1241,16 @@ export function registerByKaranteliTools(server, options) {
955
1241
  });
956
1242
  server.registerTool("get_rsi_heatmap", {
957
1243
  title: "RSI heatmap: Wilder RSI(14) on eight timeframes for the top-400 crypto perps and every TradFi perp",
958
- description: "Call this when the user asks which coins are overbought or oversold, for a crypto RSI heatmap, multi-timeframe RSI, or one contract's RSI on 15m, 1h, 4h, 12h, 1d, 3d, 1w or 1M. Returns the live board for the top-400 Binance crypto perps by volume plus every TradFi perp, with overbought/oversold counts per interval. Filter by symbol or kind (crypto|tradfi), sort by an interval.",
1244
+ description: "Call this when the user asks which coins are overbought or oversold, for a crypto RSI heatmap, multi-timeframe RSI, or one contract's RSI on 15m, 1h, 4h, 12h, 1d, 3d, 1w or 1M. Returns the live board for the top-400 Binance crypto perps by volume plus every TradFi perp, with overbought/oversold counts per interval. Filter by symbol or kind (crypto|tradfi), sort by an interval. Without symbol, the first 30 rows (by volume, or by the sort interval); pass limit (up to 400) for more, or order asc with sort for the most oversold first.",
959
1245
  inputSchema: {
960
1246
  symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,24}$/).optional().describe("One Binance symbol, e.g. BTCUSDT"),
961
1247
  kind: z.enum(["crypto", "tradfi"]).optional().describe("crypto | tradfi"),
962
1248
  sort: z.enum(["15m", "1h", "4h", "12h", "1d", "3d", "1w", "1M"]).optional().describe("Interval to sort by, descending"),
1249
+ order: z.enum(["desc", "asc"]).optional().describe("Optional. With sort: desc (default, most overbought first) or asc (most oversold first)."),
1250
+ limit: z.coerce.number().int().min(1).max(400).optional().describe("Optional. Rows without symbol (default 30)."),
963
1251
  },
964
1252
  annotations: READ_ONLY,
965
- }, async ({ symbol, kind, sort }) => {
1253
+ }, async ({ symbol, kind, sort, order, limit }) => {
966
1254
  try {
967
1255
  const params = new URLSearchParams();
968
1256
  if (symbol)
@@ -973,7 +1261,18 @@ export function registerByKaranteliTools(server, options) {
973
1261
  params.set("sort", sort);
974
1262
  const qs = params.toString();
975
1263
  const path = `/api/public/rsi${qs ? `?${qs}` : ""}`;
976
- return ok(withProvenance(await fetchJson(path), path));
1264
+ const data = await fetchJson(path);
1265
+ if (symbol)
1266
+ return ok(withProvenance(data, path));
1267
+ return ok(compact(data, path, (rec, t) => {
1268
+ let rows = list(rec.rows);
1269
+ if (sort && order === "asc") {
1270
+ /* The route sorts descending; reversed, rows without a reading on that interval stay last. */
1271
+ const has = (r) => isRec(r) && isRec(r.rsi) && typeof r.rsi[sort] === "number";
1272
+ rows = [...rows.filter(has).reverse(), ...rows.filter((r) => !has(r))];
1273
+ }
1274
+ rec.rows = t.head(rows, limit ?? 30, "rows", "pass symbol, kind, or limit up to 400 (with sort and order asc for the lowest readings)");
1275
+ }, "rows"));
977
1276
  }
978
1277
  catch (err) {
979
1278
  return fail(err);
@@ -998,13 +1297,14 @@ export function registerByKaranteliTools(server, options) {
998
1297
  });
999
1298
  server.registerTool("get_hl_whales", {
1000
1299
  title: "Hyperliquid whale tracker: top-300 accounts, long vs short, positions, changes",
1001
- description: "Call this when the user asks what Hyperliquid whales are doing, whether the biggest Hyperliquid accounts are net long or short a coin, for the largest open positions with liquidation prices, or what large accounts just opened, closed or flipped. Returns the live board of the 300 largest accounts by equity (scanned every 5 minutes, addresses only) and with events the last 200 position changes.",
1300
+ description: "Call this when the user asks what Hyperliquid whales are doing, whether the biggest Hyperliquid accounts are net long or short a coin, for the largest open positions with liquidation prices, or what large accounts just opened, closed or flipped. Returns the live board of the 300 largest accounts by equity (scanned every 5 minutes, addresses only) and with events the last 200 position changes. Without coin, the 30 largest coins and positions; pass coin, or limit (up to 200) for more.",
1002
1301
  inputSchema: {
1003
1302
  coin: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{1,24}$/).optional().describe("One coin, e.g. BTC"),
1004
1303
  events: z.boolean().optional().describe("Include the last 200 position change events"),
1304
+ limit: z.coerce.number().int().min(1).max(200).optional().describe("Optional. Coins and positions without coin, largest first (default 30)."),
1005
1305
  },
1006
1306
  annotations: READ_ONLY,
1007
- }, async ({ coin, events }) => {
1307
+ }, async ({ coin, events, limit }) => {
1008
1308
  try {
1009
1309
  const params = new URLSearchParams();
1010
1310
  if (coin)
@@ -1013,7 +1313,13 @@ export function registerByKaranteliTools(server, options) {
1013
1313
  params.set("events", "1");
1014
1314
  const qs = params.toString();
1015
1315
  const path = `/api/public/hyperliquid-whales${qs ? `?${qs}` : ""}`;
1016
- return ok(withProvenance(await fetchJson(path), path));
1316
+ const data = await fetchJson(path);
1317
+ if (coin)
1318
+ return ok(withProvenance(data, path));
1319
+ return ok(compact(data, path, (rec, t) => {
1320
+ rec.by_coin = t.head(rec.by_coin, limit ?? 30, "by_coin", "pass coin, or limit up to 200");
1321
+ rec.positions = t.head(rec.positions, limit ?? 30, "positions", "pass coin, or limit up to 200");
1322
+ }, "positions"));
1017
1323
  }
1018
1324
  catch (err) {
1019
1325
  return fail(err);
@@ -1021,15 +1327,47 @@ export function registerByKaranteliTools(server, options) {
1021
1327
  });
1022
1328
  server.registerTool("get_positioning", {
1023
1329
  title: "Positioning: long/short ratios, taker buy/sell and CVD across exchanges",
1024
- description: "Call this when the user asks about the long/short ratio, whether retail or top traders are net long or short, the taker buy/sell ratio, or CVD (cumulative volume delta) for a perpetual. Returns exchange-published statistics for the 30 most traded Binance USDT perps on every perpetual venue the positioning board records (Binance global and top-trader ratios, Bybit share long, OKX ratios and taker volume, Gate account and top-trader ratios, HTX elite ratios, Bitget account and position ratios) and CVD series for BTC, ETH and SOL; refreshed every 15 minutes.",
1330
+ description: "Call this when the user asks about the long/short ratio, whether retail or top traders are net long or short, the taker buy/sell ratio, or CVD (cumulative volume delta) for a perpetual. Returns exchange-published statistics for the 30 most traded Binance USDT perps on every perpetual venue the positioning board records (Binance global and top-trader ratios, Bybit share long, OKX ratios and taker volume, Gate account and top-trader ratios, HTX elite ratios, Bitget account and position ratios) and CVD series for BTC, ETH and SOL; refreshed every 15 minutes. Without symbol, the 10 most traded perps and the newest 24 points of each CVD series; pass symbol, limit or cvd_points for more.",
1025
1331
  inputSchema: {
1026
1332
  symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,24}$/).optional().describe("One Binance symbol, e.g. BTCUSDT"),
1333
+ limit: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Perps without symbol, most traded first (default 10, up to 30)."),
1334
+ cvd_points: z.coerce.number().int().min(1).max(288).optional().describe("Optional. Newest points per CVD series (1h and 5m); default 24 without symbol, every point with one."),
1027
1335
  },
1028
1336
  annotations: READ_ONLY,
1029
- }, async ({ symbol }) => {
1337
+ }, async ({ symbol, limit, cvd_points }) => {
1030
1338
  try {
1031
1339
  const path = `/api/public/positioning${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ""}`;
1032
- return ok(withProvenance(await fetchJson(path), path));
1340
+ const data = await fetchJson(path);
1341
+ const points = cvd_points ?? (symbol ? undefined : 24);
1342
+ if (symbol && points === undefined)
1343
+ return ok(withProvenance(data, path));
1344
+ return ok(compact(data, path, (rec, t) => {
1345
+ if (!symbol)
1346
+ rec.rows = t.head(rec.rows, limit ?? 10, "rows", "pass symbol, or limit up to 30");
1347
+ if (points !== undefined && isRec(rec.cvd)) {
1348
+ const kept = {};
1349
+ const total = {};
1350
+ const cvd = {};
1351
+ for (const [sym, intervals] of Object.entries(rec.cvd)) {
1352
+ if (!isRec(intervals)) {
1353
+ cvd[sym] = intervals;
1354
+ continue;
1355
+ }
1356
+ const out = {};
1357
+ for (const [interval, series] of Object.entries(intervals)) {
1358
+ const all = list(series);
1359
+ total[interval] = (total[interval] ?? 0) + all.length;
1360
+ kept[interval] = (kept[interval] ?? 0) + Math.min(all.length, points);
1361
+ /* Oldest point first, so the newest are the tail. */
1362
+ out[interval] = all.length > points ? all.slice(-points) : series;
1363
+ }
1364
+ cvd[sym] = out;
1365
+ }
1366
+ rec.cvd = cvd;
1367
+ for (const interval of Object.keys(total))
1368
+ t.note(`cvd.*.${interval}`, kept[interval], total[interval], "pass symbol for its full CVD, or cvd_points up to 288");
1369
+ }
1370
+ }, symbol ? undefined : "rows"));
1033
1371
  }
1034
1372
  catch (err) {
1035
1373
  return fail(err);
@@ -1037,13 +1375,14 @@ export function registerByKaranteliTools(server, options) {
1037
1375
  });
1038
1376
  server.registerTool("get_turkey_premium", {
1039
1377
  title: "Turkey Premium Index: what lira buyers pay above the world price, with TRY reference prices from Turkish venues",
1040
- description: "Call this when the user asks about Bitcoin, Ether or USDT prices in Turkish lira, the Turkey premium, the USDT/TRY rate or dollar premium in Turkey, or which Turkish exchanges (BtcTurk, Bitlo, CoinTR, OKX TR, Binance TR, Bybit TR, Bitexen; KuCoin TR contributes the USDT pairs only) trade above or below the global price. Returns the live board: the Turkey Premium Index (what a lira buyer pays for bitcoin against the global dollar price at the official exchange rate, in bps) with its dollar leg and crypto leg, a 0-100 score (50 = world price) and regime, 24h and 7d averages and the same-sign streak; then five reference prices (median of eligible order books), per-venue book status, spread, depth and each venue's implied premium. Pass pair and history_days for 15-minute history of one pair.",
1378
+ description: "Call this when the user asks about Bitcoin, Ether or USDT prices in Turkish lira, the Turkey premium, the USDT/TRY rate or dollar premium in Turkey, or which Turkish exchanges (BtcTurk, Bitlo, CoinTR, OKX TR, Binance TR, Bybit TR, Bitexen; KuCoin TR contributes the USDT pairs only) trade above or below the global price. Returns the live board: the Turkey Premium Index (what a lira buyer pays for bitcoin against the global dollar price at the official exchange rate, in bps) with its dollar leg and crypto leg, a 0-100 score (50 = world price) and regime, 24h and 7d averages and the same-sign streak; then five reference prices (median of eligible order books), per-venue book status, spread, depth and each venue's implied premium. Pass pair and history_days for 15-minute history of one pair. Without pair, the daily rows cover the last 7 days; pass pair, or daily_days (up to 30) for more.",
1041
1379
  inputSchema: {
1042
1380
  pair: z.string().trim().toUpperCase().regex(/^(BTC-TRY|ETH-TRY|USDT-TRY|BTC-USDT|ETH-USDT)$/).optional().describe("Pair: BTC-TRY, ETH-TRY, USDT-TRY, BTC-USDT or ETH-USDT"),
1043
1381
  history_days: z.number().int().min(1).max(30).optional().describe("Include 15-minute index history for the pair, 1..30 days"),
1382
+ daily_days: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Days of daily rows, newest first (default 7 without pair, every recorded day with one)."),
1044
1383
  },
1045
1384
  annotations: READ_ONLY,
1046
- }, async ({ pair, history_days }) => {
1385
+ }, async ({ pair, history_days, daily_days }) => {
1047
1386
  try {
1048
1387
  const q = new URLSearchParams();
1049
1388
  if (pair)
@@ -1052,7 +1391,17 @@ export function registerByKaranteliTools(server, options) {
1052
1391
  q.set("days", String(history_days));
1053
1392
  const qs = q.toString();
1054
1393
  const path = `/api/public/turkey-premium${qs ? `?${qs}` : ""}`;
1055
- return ok(withProvenance(await fetchJson(path), path));
1394
+ const data = await fetchJson(path);
1395
+ const wantDays = daily_days ?? (pair ? undefined : 7);
1396
+ if (wantDays === undefined)
1397
+ return ok(withProvenance(data, path));
1398
+ return ok(compact(data, path, (rec, t) => {
1399
+ const daily = list(rec.daily);
1400
+ const keep = newestDates(daily, wantDays);
1401
+ const kept = daily.filter((r) => isRec(r) && keep.has(String(r.date)));
1402
+ t.note("daily", kept.length, daily.length, "pass pair, or daily_days up to 30");
1403
+ rec.daily = kept;
1404
+ }));
1056
1405
  }
1057
1406
  catch (err) {
1058
1407
  return fail(err);
@@ -1207,13 +1556,14 @@ export function registerByKaranteliTools(server, options) {
1207
1556
  });
1208
1557
  server.registerTool("get_tradfi_gaps", {
1209
1558
  title: "Weekend and overnight gaps: stock, index and commodity perpetuals against the cash close, checkpoint by checkpoint",
1210
- description: "Call this when the user asks what a stock, index or commodity perpetual did over the weekend or overnight while the cash market was closed, how far it sat from the last cash close at each checkpoint, how the venues disagreed, and what gap the next open then realised. From ByKaranteli's own ten-minute venue record; window weekend or night; one symbol or the whole board.",
1559
+ description: "Call this when the user asks what a stock, index or commodity perpetual did over the weekend or overnight while the cash market was closed, how far it sat from the last cash close at each checkpoint, how the venues disagreed, and what gap the next open then realised. From ByKaranteli's own ten-minute venue record; window weekend or night; one symbol or the whole board. Without symbol, the first 15 underlyings of the summary and of the latest window; pass symbol, or limit (up to 60) for more.",
1211
1560
  inputSchema: {
1212
1561
  window: z.enum(["weekend", "night"]).optional().describe("weekend (Friday close to Monday open) or night (cash close to next open); default weekend"),
1213
1562
  symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9.]{1,24}$/).optional().describe("One underlying, e.g. TSLA or XAU (default: the whole board)"),
1563
+ limit: z.coerce.number().int().min(1).max(60).optional().describe("Optional. Underlyings without symbol, in the board's order, for the summary and the latest window (default 15, up to 60)."),
1214
1564
  },
1215
1565
  annotations: READ_ONLY,
1216
- }, async ({ window, symbol }) => {
1566
+ }, async ({ window, symbol, limit }) => {
1217
1567
  try {
1218
1568
  const q = new URLSearchParams();
1219
1569
  if (window !== undefined)
@@ -1222,7 +1572,15 @@ export function registerByKaranteliTools(server, options) {
1222
1572
  q.set("symbol", String(symbol));
1223
1573
  const qs = q.toString();
1224
1574
  const path = `/api/public/tradfi-gaps${qs ? `?${qs}` : ""}`;
1225
- return ok(withProvenance(await fetchJson(path), path));
1575
+ const data = await fetchJson(path);
1576
+ if (symbol !== undefined)
1577
+ return ok(withProvenance(data, path));
1578
+ const n = limit ?? 15;
1579
+ return ok(compact(data, path, (rec, t) => {
1580
+ rec.summary = t.head(rec.summary, n, "summary", "pass symbol for every window of one underlying, or limit up to 60");
1581
+ if (isRec(rec.latest))
1582
+ rec.latest = { ...rec.latest, symbols: t.head(rec.latest.symbols, n, "latest.symbols", "pass symbol for every window of one underlying, or limit up to 60") };
1583
+ }, "summary"));
1226
1584
  }
1227
1585
  catch (err) {
1228
1586
  return fail(err);
@@ -1230,16 +1588,17 @@ export function registerByKaranteliTools(server, options) {
1230
1588
  });
1231
1589
  server.registerTool("get_solana_perps", {
1232
1590
  title: "Solana Perps board: open interest, 24h volume and hourly rates across six Solana perpetual venues (Jupiter, Pacifica, Phoenix, GM Trade, Velocity, Bullet)",
1233
- description: "Call this when the user asks about perpetuals on Solana as a whole, which Solana perp DEX has the most open interest or volume, a market on Pacifica, Phoenix, GM Trade (GMX on Solana), Velocity (the Drift relaunch) or Bullet (funding, open interest, 24h volume, mark), or how Jupiter compares with the order-book venues. Returns the board read every 10 minutes: per-venue totals (one-sided open interest, both sides on pool venues, 24h volume, market count, median hourly rate, as_of), every market of every venue largest first with instrument type (perpetual, equity, index, commodity, fx), and optional hourly history of one market on any venue but Jupiter (7 days free, 30 with member depth; Jupiter history is get_jupiter_perps).",
1591
+ description: "Call this when the user asks about perpetuals on Solana as a whole, which Solana perp DEX has the most open interest or volume, a market on Pacifica, Phoenix, GM Trade (GMX on Solana), Velocity (the Drift relaunch) or Bullet (funding, open interest, 24h volume, mark), or how Jupiter compares with the order-book venues. Returns the board read every 10 minutes: per-venue totals (one-sided open interest, both sides on pool venues, 24h volume, market count, median hourly rate, as_of), every market of every venue largest first with instrument type (perpetual, equity, index, commodity, fx), and optional hourly history of one market on any venue but Jupiter (7 days free, 30 with member depth; Jupiter history is get_jupiter_perps). Without venue, the 30 largest markets; pass venue, or limit (up to 300) for more.",
1234
1592
  inputSchema: {
1235
1593
  venue: z.string().trim().toLowerCase().regex(/^(jupiter|pacifica|phoenix|gmtrade|velocity|bullet)$/).optional().describe("Venue filter: jupiter, pacifica, phoenix, gmtrade, velocity or bullet"),
1236
1594
  /* Passed as typed: Pacifica and Phoenix list mixed-case markets (kBONK, kPEPE, kSHIB) and the route matches the
1237
1595
  symbol case-insensitively, so upper-casing here would only change what the caller sees echoed back. */
1238
1596
  symbol: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,24}$/).optional().describe("Market symbol on the chosen venue (Pacifica when venue is not given) for hourly history, as the board lists it, e.g. SOL, SOL-PERP, SOL-USD, kBONK"),
1239
1597
  history_days: z.number().int().min(1).max(30).optional().describe("Hourly history for the symbol, 1..30 days"),
1598
+ limit: z.coerce.number().int().min(1).max(300).optional().describe("Optional. Markets without venue, largest first (default 30)."),
1240
1599
  },
1241
1600
  annotations: READ_ONLY,
1242
- }, async ({ venue, symbol, history_days }) => {
1601
+ }, async ({ venue, symbol, history_days, limit }) => {
1243
1602
  try {
1244
1603
  const q = new URLSearchParams();
1245
1604
  if (venue)
@@ -1250,7 +1609,12 @@ export function registerByKaranteliTools(server, options) {
1250
1609
  q.set("days", String(history_days));
1251
1610
  const qs = q.toString();
1252
1611
  const path = `/api/public/solana-perps${qs ? `?${qs}` : ""}`;
1253
- return ok(withProvenance(await fetchJson(path), path));
1612
+ const data = await fetchJson(path);
1613
+ if (venue)
1614
+ return ok(withProvenance(data, path));
1615
+ return ok(compact(data, path, (rec, t) => {
1616
+ rec.markets = t.head(rec.markets, limit ?? 30, "markets", "pass venue for every market of one venue, or limit up to 300");
1617
+ }, "markets"));
1254
1618
  }
1255
1619
  catch (err) {
1256
1620
  return fail(err);
@@ -1258,13 +1622,38 @@ export function registerByKaranteliTools(server, options) {
1258
1622
  });
1259
1623
  server.registerTool("get_coverage", {
1260
1624
  title: "Coverage registry: which venues and data types we collect, how, and how fresh",
1261
- description: "Call this when the user asks which exchanges sit behind a ByKaranteli number, whether a feed is complete or sampled, since when a venue is collected, or how fresh the data is. Returns the live coverage registry: liquidation feeds per venue with kind and last record, snapshot feeds per venue and market, funding arbitrage legs, positioning sources, whale tape, spot minutes and the Hyperliquid whale scan with freshness. snapshots[].country is the jurisdiction only when the venue states one; it is null for most venues, so do not read null as unknown risk.",
1262
- inputSchema: {},
1625
+ description: "Call this when the user asks which exchanges sit behind a ByKaranteli number, whether a feed is complete or sampled, since when a venue is collected, or how fresh the data is. Returns the live coverage registry: liquidation feeds per venue with kind and last record, snapshot feeds per venue and market, funding arbitrage legs, positioning sources, whale tape, spot minutes and the Hyperliquid whale scan with freshness. snapshots[].country is the jurisdiction only when the venue states one; it is null for most venues, so do not read null as unknown risk. By default the first 30 snapshot feeds, English notes only and no per-venue matrix cells; pass detail=true for the full registry.",
1626
+ inputSchema: {
1627
+ detail: z.boolean().optional().describe("Optional. true: every snapshot feed, the zh, ru and ko notes and the venue by product matrix cells (default false)."),
1628
+ },
1263
1629
  annotations: READ_ONLY,
1264
- }, async () => {
1630
+ }, async ({ detail }) => {
1265
1631
  try {
1266
1632
  const path = "/api/public/coverage";
1267
- return ok(withProvenance(await fetchJson(path), path));
1633
+ const data = await fetchJson(path);
1634
+ if (detail)
1635
+ return ok(withProvenance(data, path));
1636
+ const more = "pass detail=true for the full registry";
1637
+ return ok(compact(data, path, (rec, t) => {
1638
+ rec.snapshots = t.head(rec.snapshots, 30, "snapshots", more);
1639
+ const liq = list(rec.liquidations);
1640
+ let translated = 0;
1641
+ rec.liquidations = liq.map((r) => {
1642
+ if (!isRec(r))
1643
+ return r;
1644
+ /* note_en stays; the zh, ru and ko notes translate it for the site's other locales. */
1645
+ const { note_zh, note_ru, note_ko, ...rest } = r;
1646
+ if (note_zh !== undefined || note_ru !== undefined || note_ko !== undefined)
1647
+ translated += 1;
1648
+ return rest;
1649
+ });
1650
+ t.note("liquidations[].note_zh, note_ru, note_ko", 0, translated, more);
1651
+ if (isRec(rec.matrix) && isRec(rec.matrix.cells)) {
1652
+ const products = Object.keys(rec.matrix.cells).length;
1653
+ rec.matrix = { ...rec.matrix, cells: {} };
1654
+ t.note("matrix.cells", 0, products, `${more}; matrix.products already lists each product's venues`);
1655
+ }
1656
+ }));
1268
1657
  }
1269
1658
  catch (err) {
1270
1659
  return fail(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bykaranteli-mcp",
3
- "version": "0.34.6",
3
+ "version": "0.35.0",
4
4
  "description": "MCP server for live crypto derivatives data from bykaranteli.com: funding rates, funding arbitrage, open interest pressure, liquidations, options, ETF flows, Fear & Greed and BTC dominance. 67 read-only and account tools (the alert and watchlist tools act on your own account), free account key, paid depth via plans or x402.",
5
5
  "type": "module",
6
6
  "bin": {