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 +14 -0
- package/README.md +2 -0
- package/dist/tools.js +460 -71
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|