bykaranteli-mcp 0.34.6 → 0.35.1
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 +24 -0
- package/README.md +2 -0
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +479 -83
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.1] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
67 tools.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Every tool carries `annotations.title`, the same text as its top-level `title`: directory portals (the Claude Connectors Directory among them) read the display name there. The read-only, destructive, idempotent and open-world hints are unchanged.
|
|
12
|
+
- Descriptions name plans, not keys: depth and limits follow the account's plan whether the client signed in with OAuth or sends an account key (`get_liqmap`, `get_series`, `get_options_chain`, `get_solana_perps`, the account tools).
|
|
13
|
+
- `get_series` returns the newest 300 bars when neither `from` nor `limit` is given (Builder and higher plans received 1,000 by default, 64,700 characters for price); `limit` and `from`/`to` still return up to 5,000.
|
|
14
|
+
|
|
15
|
+
## [0.35.0] - 2026-10-11
|
|
16
|
+
|
|
17
|
+
67 tools.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- 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`.
|
|
28
|
+
|
|
5
29
|
## [0.34.6] - 2026-10-09
|
|
6
30
|
|
|
7
31
|
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.d.ts
CHANGED
|
@@ -25,4 +25,4 @@ export type RegisterOptions = {
|
|
|
25
25
|
* (the hosted endpoint). Takes precedence over apiKey when it returns one. */
|
|
26
26
|
authorizationProvider?: () => string | undefined;
|
|
27
27
|
};
|
|
28
|
-
export declare function registerByKaranteliTools(
|
|
28
|
+
export declare function registerByKaranteliTools(target: McpServer, options?: RegisterOptions): void;
|
package/dist/tools.js
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
8
|
import { buildProvenance, pageForApiPath } from "./provenance.js";
|
|
9
|
-
export function registerByKaranteliTools(
|
|
9
|
+
export function registerByKaranteliTools(target, options) {
|
|
10
|
+
const withTitle = (def) => def.title && !(def.annotations && "title" in def.annotations) ? { ...def, annotations: { ...(def.annotations ?? {}), title: def.title } } : def;
|
|
11
|
+
const server = {
|
|
12
|
+
registerTool: (name, def, handler) => target.registerTool.call(target, name, withTitle(def), handler),
|
|
13
|
+
};
|
|
10
14
|
const BASE_URL = (options?.baseUrl ?? process.env.BYKARANTELI_BASE_URL ?? "https://bykaranteli.com").replace(/\/+$/, "");
|
|
11
15
|
const PUBLIC_URL = (options?.publicUrl ?? process.env.BYKARANTELI_PUBLIC_URL ?? "https://bykaranteli.com").replace(/\/+$/, "");
|
|
12
16
|
const USER_AGENT = `bykaranteli-mcp/${options?.version ?? "dev"} (+https://github.com/bykarantelicom/bykaranteli-mcp)`;
|
|
@@ -76,7 +80,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
76
80
|
* context window, with no signal that anything was cut (audit 2026-09-04
|
|
77
81
|
* #41). Above the ceiling the tool answers with guidance instead of a
|
|
78
82
|
* payload the caller could not use anyway. */
|
|
79
|
-
const MAX_RESULT_CHARS =
|
|
83
|
+
const MAX_RESULT_CHARS = 100_000;
|
|
80
84
|
function ok(data) {
|
|
81
85
|
const text = JSON.stringify(data);
|
|
82
86
|
if (text.length <= MAX_RESULT_CHARS)
|
|
@@ -93,6 +97,47 @@ export function registerByKaranteliTools(server, options) {
|
|
|
93
97
|
}],
|
|
94
98
|
};
|
|
95
99
|
}
|
|
100
|
+
const isRec = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
101
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
102
|
+
class Trim {
|
|
103
|
+
cuts = [];
|
|
104
|
+
/** The first n items in the route's own order; anything that is not a longer list passes through. */
|
|
105
|
+
head(value, n, field, more) {
|
|
106
|
+
if (!Array.isArray(value) || value.length <= n)
|
|
107
|
+
return value;
|
|
108
|
+
this.cuts.push({ field, returned: n, total: value.length, more });
|
|
109
|
+
return value.slice(0, n);
|
|
110
|
+
}
|
|
111
|
+
/** One entry for a cut spread over several lists (a field inside every row). */
|
|
112
|
+
note(field, returned, total, more) {
|
|
113
|
+
if (returned < total)
|
|
114
|
+
this.cuts.push({ field, returned, total, more });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/* The answer with provenance already attached, shortened by `cut`; rows_total and rows_returned describe the main
|
|
118
|
+
* list when it is named (a route that already counts its rows keeps its own rows_total). */
|
|
119
|
+
function trimmedAnswer(full, cut, rowsField) {
|
|
120
|
+
if (!isRec(full))
|
|
121
|
+
return full;
|
|
122
|
+
const rec = { ...full };
|
|
123
|
+
const total = rowsField && Array.isArray(rec[rowsField]) ? rec[rowsField].length : undefined;
|
|
124
|
+
const t = new Trim();
|
|
125
|
+
cut(rec, t);
|
|
126
|
+
if (rowsField && total !== undefined && Array.isArray(rec[rowsField])) {
|
|
127
|
+
if (rec.rows_total === undefined)
|
|
128
|
+
rec.rows_total = total;
|
|
129
|
+
rec.rows_returned = rec[rowsField].length;
|
|
130
|
+
}
|
|
131
|
+
return t.cuts.length > 0 ? { ...rec, trimmed: t.cuts } : rec;
|
|
132
|
+
}
|
|
133
|
+
function compact(data, path, cut, rowsField) {
|
|
134
|
+
return trimmedAnswer(withProvenance(data, path), cut, rowsField);
|
|
135
|
+
}
|
|
136
|
+
/** The newest `days` distinct dates of rows carrying a `date` field, in the route's order. */
|
|
137
|
+
function newestDates(rows, days) {
|
|
138
|
+
const dates = [...new Set(rows.map((r) => (isRec(r) ? String(r.date) : "")))].filter(Boolean).sort().reverse();
|
|
139
|
+
return new Set(dates.slice(0, days));
|
|
140
|
+
}
|
|
96
141
|
function fail(err) {
|
|
97
142
|
const message = err instanceof Error ? err.message : String(err);
|
|
98
143
|
const permanent = /returned HTTP 4\d\d/.test(message);
|
|
@@ -150,7 +195,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
150
195
|
});
|
|
151
196
|
server.registerTool("get_liquidations", {
|
|
152
197
|
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.",
|
|
198
|
+
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
199
|
inputSchema: {
|
|
155
200
|
symbol: z
|
|
156
201
|
.string()
|
|
@@ -160,9 +205,10 @@ export function registerByKaranteliTools(server, options) {
|
|
|
160
205
|
.optional()
|
|
161
206
|
.describe("Optional symbol filter like BTCUSDT or ETHUSDT. Omit for all symbols."),
|
|
162
207
|
days: z.number().int().min(1).max(90).optional().describe("How many most recent days to return (default 7)."),
|
|
208
|
+
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
209
|
},
|
|
164
210
|
annotations: READ_ONLY,
|
|
165
|
-
}, async ({ symbol, days }) => {
|
|
211
|
+
}, async ({ symbol, days, limit }) => {
|
|
166
212
|
try {
|
|
167
213
|
const d = (await fetchJson("/api/v1/public/datasets/liquidations-daily.json"));
|
|
168
214
|
const rows = Array.isArray(d.rows) ? d.rows : [];
|
|
@@ -199,7 +245,8 @@ export function registerByKaranteliTools(server, options) {
|
|
|
199
245
|
day.rows += 1;
|
|
200
246
|
byDate.set(date, day);
|
|
201
247
|
}
|
|
202
|
-
|
|
248
|
+
/* 0.35.0: 30 rows without a symbol (the 400-row page was 78k characters); a symbol keeps the old 400. */
|
|
249
|
+
const MAX_ROWS = limit ?? (wantSymbol ? 400 : 30);
|
|
203
250
|
/* Largest rows first within the cap, newest day first. */
|
|
204
251
|
const ordered = [...filtered].sort((a, b) => {
|
|
205
252
|
const dd = String(b.date).localeCompare(String(a.date));
|
|
@@ -227,14 +274,16 @@ export function registerByKaranteliTools(server, options) {
|
|
|
227
274
|
})),
|
|
228
275
|
},
|
|
229
276
|
rows: ordered.slice(0, MAX_ROWS),
|
|
277
|
+
rows_total: filtered.length,
|
|
230
278
|
rows_returned: Math.min(ordered.length, MAX_ROWS),
|
|
231
279
|
rows_matched: filtered.length,
|
|
232
280
|
truncated,
|
|
233
281
|
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
|
|
282
|
+
? `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
283
|
: baseNote,
|
|
236
284
|
source: `${PUBLIC_URL}/liquidations`,
|
|
237
285
|
provenance: provenanceFor(d, `/api/v1/public/datasets/liquidations-daily.json${wantSymbol ? `?symbol=${wantSymbol}` : ""}`, "/liquidations"),
|
|
286
|
+
...(truncated ? { trimmed: [{ field: "rows", returned: MAX_ROWS, total: filtered.length, more: "pass symbol, or limit up to 400" }] } : {}),
|
|
238
287
|
});
|
|
239
288
|
}
|
|
240
289
|
catch (err) {
|
|
@@ -312,13 +361,35 @@ export function registerByKaranteliTools(server, options) {
|
|
|
312
361
|
});
|
|
313
362
|
server.registerTool("get_funding_arbitrage", {
|
|
314
363
|
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: {
|
|
364
|
+
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.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
symbol: z.string().optional().describe("Optional. One perp, e.g. BTCUSDT or BTC: its row with every venue leg."),
|
|
367
|
+
},
|
|
317
368
|
annotations: READ_ONLY,
|
|
318
|
-
}, async () => {
|
|
369
|
+
}, async ({ symbol }) => {
|
|
319
370
|
try {
|
|
320
371
|
const d = (await fetchJson("/api/public/funding-arb"));
|
|
321
|
-
|
|
372
|
+
const full = { ...d, source: `${PUBLIC_URL}/funding-arb`, provenance: provenanceFor(d, "/api/public/funding-arb") };
|
|
373
|
+
const rows = list(d.rows).filter(isRec);
|
|
374
|
+
if (symbol) {
|
|
375
|
+
const want = normalizeSymbol(symbol);
|
|
376
|
+
const row = rows.find((r) => r.symbol === want);
|
|
377
|
+
if (!row)
|
|
378
|
+
return ok({ generatedAt: d.generatedAt, note: `${want} is not on the funding arbitrage board. Symbols: ${rows.map((r) => String(r.symbol)).join(", ")}` });
|
|
379
|
+
return ok({ ...full, rows: [row] });
|
|
380
|
+
}
|
|
381
|
+
const LEGS = 5;
|
|
382
|
+
return ok(trimmedAnswer(full, (rec, t) => {
|
|
383
|
+
let kept = 0;
|
|
384
|
+
let total = 0;
|
|
385
|
+
rec.rows = rows.map((r) => {
|
|
386
|
+
const legs = list(r.exchanges);
|
|
387
|
+
total += legs.length;
|
|
388
|
+
kept += Math.min(legs.length, LEGS);
|
|
389
|
+
return legs.length > LEGS ? { ...r, exchanges: legs.slice(0, LEGS) } : r;
|
|
390
|
+
});
|
|
391
|
+
t.note("rows[].exchanges", kept, total, "pass symbol for every venue leg of one perp");
|
|
392
|
+
}));
|
|
322
393
|
}
|
|
323
394
|
catch (err) {
|
|
324
395
|
return fail(err);
|
|
@@ -423,12 +494,22 @@ export function registerByKaranteliTools(server, options) {
|
|
|
423
494
|
});
|
|
424
495
|
server.registerTool("get_coinbase_premium", {
|
|
425
496
|
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: {
|
|
497
|
+
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.",
|
|
498
|
+
inputSchema: {
|
|
499
|
+
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)."),
|
|
500
|
+
},
|
|
428
501
|
annotations: READ_ONLY,
|
|
429
|
-
}, async () => {
|
|
502
|
+
}, async ({ days }) => {
|
|
430
503
|
try {
|
|
431
|
-
return ok(
|
|
504
|
+
return ok(compact(await fetchJson("/api/public/premium"), "/api/public/premium", (rec, t) => {
|
|
505
|
+
if (!isRec(rec.kimchi))
|
|
506
|
+
return;
|
|
507
|
+
const daily = list(rec.kimchi.daily);
|
|
508
|
+
const keep = newestDates(daily, days ?? 7);
|
|
509
|
+
const kept = daily.filter((r) => isRec(r) && keep.has(String(r.date)));
|
|
510
|
+
t.note("kimchi.daily", kept.length, daily.length, "pass days up to 90");
|
|
511
|
+
rec.kimchi = { ...rec.kimchi, daily: kept };
|
|
512
|
+
}));
|
|
432
513
|
}
|
|
433
514
|
catch (err) {
|
|
434
515
|
return fail(err);
|
|
@@ -551,7 +632,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
551
632
|
});
|
|
552
633
|
server.registerTool("get_tokenized_stocks", {
|
|
553
634
|
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.",
|
|
635
|
+
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
636
|
inputSchema: {
|
|
556
637
|
underlying: z.string().regex(/^[A-Za-z0-9.]{1,16}$/).optional().describe("Stock ticker, e.g. TSLA; omit for the whole board."),
|
|
557
638
|
/* Issuer ids mirror TOKENIZED_ISSUER_IDS in web/src/lib/tokenized-equities/meta.ts (the hosted tool reads that list
|
|
@@ -561,7 +642,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
561
642
|
.optional()
|
|
562
643
|
.describe("One issuer: xstocks, robinhood, coinbase, bstocks, ondo, backpack, gstocks, reality, superstate, figure, dinari or securitize; omit for every issuer."),
|
|
563
644
|
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
|
|
645
|
+
top: z.number().int().min(1).max(200).optional().describe("Rows by supply (default 10, max 200)."),
|
|
565
646
|
},
|
|
566
647
|
annotations: READ_ONLY,
|
|
567
648
|
}, async ({ underlying, issuer, chain, top }) => {
|
|
@@ -573,9 +654,32 @@ export function registerByKaranteliTools(server, options) {
|
|
|
573
654
|
params.set("issuer", issuer);
|
|
574
655
|
if (chain)
|
|
575
656
|
params.set("chain", chain);
|
|
576
|
-
|
|
657
|
+
/* 0.35.0: 10 rows by default (20 full rows were 113k characters). */
|
|
658
|
+
params.set("top", String(top ?? 10));
|
|
577
659
|
const path = `/api/public/tokenized-stocks?${params.toString()}`;
|
|
578
|
-
|
|
660
|
+
const data = await fetchJson(path);
|
|
661
|
+
if (underlying)
|
|
662
|
+
return ok(withProvenance(data, path));
|
|
663
|
+
const N = 3;
|
|
664
|
+
return ok(compact(data, path, (rec, t) => {
|
|
665
|
+
const kept = { wrappers: 0, cex: 0 };
|
|
666
|
+
const total = { wrappers: 0, cex: 0 };
|
|
667
|
+
rec.rows = list(rec.rows).map((r) => {
|
|
668
|
+
if (!isRec(r))
|
|
669
|
+
return r;
|
|
670
|
+
const out = { ...r };
|
|
671
|
+
for (const key of ["wrappers", "cex"]) {
|
|
672
|
+
const items = list(r[key]);
|
|
673
|
+
total[key] += items.length;
|
|
674
|
+
kept[key] += Math.min(items.length, N);
|
|
675
|
+
if (items.length > N)
|
|
676
|
+
out[key] = items.slice(0, N);
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
679
|
+
});
|
|
680
|
+
t.note("rows[].wrappers", kept.wrappers, total.wrappers, "pass underlying for every wrapper of one stock");
|
|
681
|
+
t.note("rows[].cex", kept.cex, total.cex, "pass underlying for every exchange pair of one stock");
|
|
682
|
+
}, "rows"));
|
|
579
683
|
}
|
|
580
684
|
catch (err) {
|
|
581
685
|
return fail(err);
|
|
@@ -680,12 +784,42 @@ export function registerByKaranteliTools(server, options) {
|
|
|
680
784
|
});
|
|
681
785
|
server.registerTool("get_borrow_rates", {
|
|
682
786
|
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: {
|
|
787
|
+
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.",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
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."),
|
|
790
|
+
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)."),
|
|
791
|
+
},
|
|
685
792
|
annotations: READ_ONLY,
|
|
686
|
-
}, async () => {
|
|
793
|
+
}, async ({ asset, hours }) => {
|
|
687
794
|
try {
|
|
688
|
-
|
|
795
|
+
const path = "/api/public/borrow-rates";
|
|
796
|
+
return ok(compact(await fetchJson(path), path, (rec, t) => {
|
|
797
|
+
const byAsset = (r) => !asset || (isRec(r) && String(r.asset).toUpperCase() === asset);
|
|
798
|
+
if (asset) {
|
|
799
|
+
rec.latest = list(rec.latest).filter(byAsset);
|
|
800
|
+
rec.carry = list(rec.carry).filter(byAsset);
|
|
801
|
+
if (list(rec.latest).length === 0)
|
|
802
|
+
rec.note = `${asset} has no recorded borrow rate; omit asset for every recorded one.`;
|
|
803
|
+
}
|
|
804
|
+
const series = list(rec.series).filter(byAsset).filter(isRec);
|
|
805
|
+
/* Hourly points [ts_ms, value], oldest first; the window ends at the newest point of any series. */
|
|
806
|
+
let newest = 0;
|
|
807
|
+
for (const s of series)
|
|
808
|
+
for (const p of list(s.points))
|
|
809
|
+
if (Array.isArray(p) && Number(p[0]) > newest)
|
|
810
|
+
newest = Number(p[0]);
|
|
811
|
+
const cutoff = newest - (hours ?? (asset ? 720 : 24)) * 3_600_000;
|
|
812
|
+
let kept = 0;
|
|
813
|
+
let total = 0;
|
|
814
|
+
rec.series = series.map((s) => {
|
|
815
|
+
const points = list(s.points);
|
|
816
|
+
const inWindow = points.filter((p) => Array.isArray(p) && Number(p[0]) > cutoff);
|
|
817
|
+
total += points.length;
|
|
818
|
+
kept += inWindow.length;
|
|
819
|
+
return { ...s, points: inWindow };
|
|
820
|
+
});
|
|
821
|
+
t.note("series[].points", kept, total, "pass hours up to 720, or asset for one asset's full series");
|
|
822
|
+
}));
|
|
689
823
|
}
|
|
690
824
|
catch (err) {
|
|
691
825
|
return fail(err);
|
|
@@ -706,12 +840,26 @@ export function registerByKaranteliTools(server, options) {
|
|
|
706
840
|
});
|
|
707
841
|
server.registerTool("get_settlements", {
|
|
708
842
|
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: {
|
|
843
|
+
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).",
|
|
844
|
+
inputSchema: {
|
|
845
|
+
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."),
|
|
846
|
+
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."),
|
|
847
|
+
},
|
|
711
848
|
annotations: READ_ONLY,
|
|
712
|
-
}, async () => {
|
|
849
|
+
}, async ({ base, limit }) => {
|
|
713
850
|
try {
|
|
714
|
-
|
|
851
|
+
const path = "/api/public/settlements";
|
|
852
|
+
return ok(compact(await fetchJson(path), path, (rec, t) => {
|
|
853
|
+
if (base) {
|
|
854
|
+
const byBase = (r) => isRec(r) && String(r.base).toUpperCase() === base;
|
|
855
|
+
rec.upcoming = list(rec.upcoming).filter(byBase);
|
|
856
|
+
rec.recorded = list(rec.recorded).filter(byBase);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const n = limit ?? 30;
|
|
860
|
+
rec.upcoming = t.head(rec.upcoming, n, "upcoming", "pass base for one asset, or limit up to 300");
|
|
861
|
+
rec.recorded = t.head(rec.recorded, n, "recorded", "pass base for one asset, or limit up to 300");
|
|
862
|
+
}, base ? undefined : "upcoming"));
|
|
715
863
|
}
|
|
716
864
|
catch (err) {
|
|
717
865
|
return fail(err);
|
|
@@ -732,10 +880,11 @@ export function registerByKaranteliTools(server, options) {
|
|
|
732
880
|
});
|
|
733
881
|
server.registerTool("get_withdrawal_status", {
|
|
734
882
|
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.",
|
|
883
|
+
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
884
|
inputSchema: {
|
|
737
885
|
asset: z.string().optional().describe("string, optional asset code, e.g. USDT"),
|
|
738
886
|
venue: z.string().optional().describe("string, optional venue id, e.g. kucoin"),
|
|
887
|
+
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
888
|
},
|
|
740
889
|
annotations: READ_ONLY,
|
|
741
890
|
}, async (args) => {
|
|
@@ -746,7 +895,15 @@ export function registerByKaranteliTools(server, options) {
|
|
|
746
895
|
if (args.venue)
|
|
747
896
|
qs.set("venue", String(args.venue).toLowerCase());
|
|
748
897
|
const q = qs.toString();
|
|
749
|
-
|
|
898
|
+
const path = `/api/public/withdrawals${q ? `?${q}` : ""}`;
|
|
899
|
+
const data = await fetchJson(path);
|
|
900
|
+
if (args.asset)
|
|
901
|
+
return ok(withProvenance(data, path));
|
|
902
|
+
const n = args.limit ?? 30;
|
|
903
|
+
return ok(compact(data, path, (rec, t) => {
|
|
904
|
+
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");
|
|
905
|
+
rec.events = t.head(rec.events, n, "events", "pass limit up to 400");
|
|
906
|
+
}, "suspended_rows"));
|
|
750
907
|
}
|
|
751
908
|
catch (err) {
|
|
752
909
|
return fail(err);
|
|
@@ -754,10 +911,11 @@ export function registerByKaranteliTools(server, options) {
|
|
|
754
911
|
});
|
|
755
912
|
server.registerTool("get_leverage_tiers", {
|
|
756
913
|
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).",
|
|
914
|
+
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
915
|
inputSchema: {
|
|
759
916
|
symbol: z.string().optional().describe("string, optional base asset, e.g. BTC"),
|
|
760
917
|
venue: z.string().optional().describe("string, optional venue id, e.g. bybit"),
|
|
918
|
+
limit: z.coerce.number().int().min(1).max(300).optional().describe("Optional. Rows per list without symbol (default 30)."),
|
|
761
919
|
},
|
|
762
920
|
annotations: READ_ONLY,
|
|
763
921
|
}, async (args) => {
|
|
@@ -768,7 +926,36 @@ export function registerByKaranteliTools(server, options) {
|
|
|
768
926
|
if (args.venue)
|
|
769
927
|
qs.set("venue", String(args.venue).toLowerCase());
|
|
770
928
|
const q = qs.toString();
|
|
771
|
-
|
|
929
|
+
const path = `/api/public/leverage-tiers${q ? `?${q}` : ""}`;
|
|
930
|
+
const data = await fetchJson(path);
|
|
931
|
+
if (args.symbol)
|
|
932
|
+
return ok(withProvenance(data, path));
|
|
933
|
+
const n = args.limit ?? 30;
|
|
934
|
+
const more = "pass symbol for every tier of one base asset, or limit up to 300";
|
|
935
|
+
return ok(compact(data, path, (rec, t) => {
|
|
936
|
+
/* venue alone: one flat list of every tier of every contract on that venue (2,000 rows on Bybit). */
|
|
937
|
+
rec.rows = t.head(rec.rows, n, "rows", more);
|
|
938
|
+
rec.compare = t.head(rec.compare, n, "compare", more);
|
|
939
|
+
rec.changes = t.head(rec.changes, n, "changes", "pass limit up to 300");
|
|
940
|
+
if (isRec(rec.ladders)) {
|
|
941
|
+
const TIERS = 5;
|
|
942
|
+
let kept = 0;
|
|
943
|
+
let total = 0;
|
|
944
|
+
const ladders = {};
|
|
945
|
+
for (const [base, venues] of Object.entries(rec.ladders)) {
|
|
946
|
+
ladders[base] = list(venues).map((v) => {
|
|
947
|
+
if (!isRec(v))
|
|
948
|
+
return v;
|
|
949
|
+
const rows = list(v.rows);
|
|
950
|
+
total += rows.length;
|
|
951
|
+
kept += Math.min(rows.length, TIERS);
|
|
952
|
+
return rows.length > TIERS ? { ...v, rows: rows.slice(0, TIERS) } : v;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
rec.ladders = ladders;
|
|
956
|
+
t.note("ladders.*[].rows", kept, total, "pass symbol for every tier of one base asset");
|
|
957
|
+
}
|
|
958
|
+
}, isRec(data) && Array.isArray(data.rows) ? "rows" : "compare"));
|
|
772
959
|
}
|
|
773
960
|
catch (err) {
|
|
774
961
|
return fail(err);
|
|
@@ -776,10 +963,11 @@ export function registerByKaranteliTools(server, options) {
|
|
|
776
963
|
});
|
|
777
964
|
server.registerTool("get_venue_markets", {
|
|
778
965
|
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.",
|
|
966
|
+
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
967
|
inputSchema: {
|
|
781
968
|
symbol: z.string().optional().describe("string, optional base asset, e.g. BTC"),
|
|
782
969
|
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"),
|
|
970
|
+
limit: z.coerce.number().int().min(1).max(250).optional().describe("Optional. Venue market rows without symbol, largest open interest first (default 30)."),
|
|
783
971
|
},
|
|
784
972
|
annotations: READ_ONLY,
|
|
785
973
|
}, async (args) => {
|
|
@@ -787,7 +975,17 @@ export function registerByKaranteliTools(server, options) {
|
|
|
787
975
|
const path = args.history_days
|
|
788
976
|
? `/api/public/venues/oi-history?days=${Math.min(90, Math.floor(args.history_days))}`
|
|
789
977
|
: args.symbol ? `/api/public/venues/markets?symbol=${encodeURIComponent(args.symbol.toUpperCase())}` : "/api/public/venues/markets";
|
|
790
|
-
|
|
978
|
+
const data = await fetchJson(path);
|
|
979
|
+
if (args.history_days || args.symbol)
|
|
980
|
+
return ok(withProvenance(data, path));
|
|
981
|
+
/* The route lists rows by venue; the board's own ranking is open interest, then 24h volume for the rows
|
|
982
|
+
* without open interest (spot). */
|
|
983
|
+
const n = Number(args.limit ?? 30);
|
|
984
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : -1);
|
|
985
|
+
return ok(compact(data, path, (rec, t) => {
|
|
986
|
+
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));
|
|
987
|
+
rec.rows = t.head(ranked, n, "rows", "pass symbol for one coin's rows on every venue, or limit up to 250");
|
|
988
|
+
}, "rows"));
|
|
791
989
|
}
|
|
792
990
|
catch (err) {
|
|
793
991
|
return fail(err);
|
|
@@ -795,12 +993,28 @@ export function registerByKaranteliTools(server, options) {
|
|
|
795
993
|
});
|
|
796
994
|
server.registerTool("get_lead_lag", {
|
|
797
995
|
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: {
|
|
996
|
+
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.",
|
|
997
|
+
inputSchema: {
|
|
998
|
+
days: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Daily rows per pair, newest kept (default 7, up to 30)."),
|
|
999
|
+
},
|
|
800
1000
|
annotations: READ_ONLY,
|
|
801
|
-
}, async () => {
|
|
1001
|
+
}, async ({ days }) => {
|
|
802
1002
|
try {
|
|
803
|
-
|
|
1003
|
+
const path = "/api/public/venues/lead-lag";
|
|
1004
|
+
return ok(compact(await fetchJson(path), path, (rec, t) => {
|
|
1005
|
+
const n = days ?? 7;
|
|
1006
|
+
let kept = 0;
|
|
1007
|
+
let total = 0;
|
|
1008
|
+
rec.pairs = list(rec.pairs).map((p) => {
|
|
1009
|
+
if (!isRec(p) || !Array.isArray(p.days))
|
|
1010
|
+
return p;
|
|
1011
|
+
/* Oldest day first, so the newest n are the tail. */
|
|
1012
|
+
total += p.days.length;
|
|
1013
|
+
kept += Math.min(p.days.length, n);
|
|
1014
|
+
return p.days.length > n ? { ...p, days: p.days.slice(-n) } : p;
|
|
1015
|
+
});
|
|
1016
|
+
t.note("pairs[].days", kept, total, "pass days up to 30");
|
|
1017
|
+
}));
|
|
804
1018
|
}
|
|
805
1019
|
catch (err) {
|
|
806
1020
|
return fail(err);
|
|
@@ -860,15 +1074,65 @@ export function registerByKaranteliTools(server, options) {
|
|
|
860
1074
|
return fail(err);
|
|
861
1075
|
}
|
|
862
1076
|
});
|
|
1077
|
+
/* Daily series boards (macro, network): a call without days reads the route's shortest window (30 days) and keeps the
|
|
1078
|
+
* newest 7 points of each series; a call with days gets the whole window it asked for. */
|
|
1079
|
+
async function dailySeries(route, days, key) {
|
|
1080
|
+
const wanted = days && days > 0 ? Math.floor(days) : undefined;
|
|
1081
|
+
const path = `${route}?days=${wanted ?? 30}`;
|
|
1082
|
+
const data = await fetchJson(path);
|
|
1083
|
+
/* series narrows the answer to one key, so a long window of one series fits where every series would not. */
|
|
1084
|
+
const narrowed = key && isRec(data) && isRec(data.series)
|
|
1085
|
+
? (() => {
|
|
1086
|
+
if (!Object.hasOwn(data.series, key))
|
|
1087
|
+
throw new InputError(`series must be one of ${Object.keys(data.series).join(", ")}.`);
|
|
1088
|
+
return { ...data, series: { [key]: data.series[key] } };
|
|
1089
|
+
})()
|
|
1090
|
+
: data;
|
|
1091
|
+
if (wanted) {
|
|
1092
|
+
const answer = withProvenance(narrowed, path);
|
|
1093
|
+
/* Every series over a long window can pass the ceiling (macro, 365 days: 146k characters); name the keys that
|
|
1094
|
+
* fit instead of the generic ceiling note. */
|
|
1095
|
+
if (!key && isRec(data) && isRec(data.series) && JSON.stringify(answer).length > MAX_RESULT_CHARS) {
|
|
1096
|
+
const keys = Object.keys(data.series);
|
|
1097
|
+
return ok({
|
|
1098
|
+
truncated: true,
|
|
1099
|
+
limit: MAX_RESULT_CHARS,
|
|
1100
|
+
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.`,
|
|
1101
|
+
series_keys: keys,
|
|
1102
|
+
...(isRec(data.latest) ? { latest: data.latest } : {}),
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return ok(answer);
|
|
1106
|
+
}
|
|
1107
|
+
return ok(compact(narrowed, path, (rec, t) => {
|
|
1108
|
+
if (!isRec(rec.series))
|
|
1109
|
+
return;
|
|
1110
|
+
const POINTS = 7;
|
|
1111
|
+
let kept = 0;
|
|
1112
|
+
let total = 0;
|
|
1113
|
+
const series = {};
|
|
1114
|
+
for (const [key, points] of Object.entries(rec.series)) {
|
|
1115
|
+
const all = list(points);
|
|
1116
|
+
total += all.length;
|
|
1117
|
+
kept += Math.min(all.length, POINTS);
|
|
1118
|
+
/* Oldest date first, so the newest points are the tail. */
|
|
1119
|
+
series[key] = all.length > POINTS ? all.slice(-POINTS) : points;
|
|
1120
|
+
}
|
|
1121
|
+
rec.series = series;
|
|
1122
|
+
t.note("series.*", kept, total, "pass days (30 to 4000) for the daily series, and series for one key over a long window");
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
863
1125
|
server.registerTool("get_macro_liquidity", {
|
|
864
1126
|
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: {
|
|
1127
|
+
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.",
|
|
1128
|
+
inputSchema: {
|
|
1129
|
+
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."),
|
|
1130
|
+
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."),
|
|
1131
|
+
},
|
|
867
1132
|
annotations: READ_ONLY,
|
|
868
1133
|
}, async (args) => {
|
|
869
1134
|
try {
|
|
870
|
-
|
|
871
|
-
return ok(withProvenance(await fetchJson(path), path));
|
|
1135
|
+
return await dailySeries("/api/public/macro", args.days, args.series);
|
|
872
1136
|
}
|
|
873
1137
|
catch (err) {
|
|
874
1138
|
return fail(err);
|
|
@@ -876,13 +1140,15 @@ export function registerByKaranteliTools(server, options) {
|
|
|
876
1140
|
});
|
|
877
1141
|
server.registerTool("get_network_health", {
|
|
878
1142
|
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: {
|
|
1143
|
+
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.",
|
|
1144
|
+
inputSchema: {
|
|
1145
|
+
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."),
|
|
1146
|
+
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."),
|
|
1147
|
+
},
|
|
881
1148
|
annotations: READ_ONLY,
|
|
882
1149
|
}, async (args) => {
|
|
883
1150
|
try {
|
|
884
|
-
|
|
885
|
-
return ok(withProvenance(await fetchJson(path), path));
|
|
1151
|
+
return await dailySeries("/api/public/network", args.days, args.series);
|
|
886
1152
|
}
|
|
887
1153
|
catch (err) {
|
|
888
1154
|
return fail(err);
|
|
@@ -890,7 +1156,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
890
1156
|
});
|
|
891
1157
|
server.registerTool("get_liqmap", {
|
|
892
1158
|
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).
|
|
1159
|
+
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). On the Free plan the 24h view; on Builder and higher plans 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
1160
|
inputSchema: {
|
|
895
1161
|
symbol: z
|
|
896
1162
|
.string()
|
|
@@ -902,17 +1168,35 @@ export function registerByKaranteliTools(server, options) {
|
|
|
902
1168
|
timeframe: z
|
|
903
1169
|
.enum(["1h", "4h", "12h", "24h", "3d", "1w", "30d"])
|
|
904
1170
|
.optional()
|
|
905
|
-
.describe("Model window. Default 24h, the only one
|
|
1171
|
+
.describe("Model window. Default 24h, the only one on the Free plan; Builder and higher plans get every window."),
|
|
1172
|
+
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
1173
|
},
|
|
907
1174
|
annotations: READ_ONLY,
|
|
908
|
-
}, async ({ symbol, timeframe }) => {
|
|
1175
|
+
}, async ({ symbol, timeframe, detail }) => {
|
|
909
1176
|
try {
|
|
910
1177
|
const raw = symbol ?? "BTCUSDT";
|
|
911
1178
|
const sym = raw.endsWith("USDT") ? raw : `${raw}USDT`;
|
|
912
1179
|
const path = `/api/liqmap/public?symbol=${encodeURIComponent(sym)}&timeframe=${encodeURIComponent(timeframe ?? "24h")}`;
|
|
913
1180
|
const data = await fetchJson(path);
|
|
914
1181
|
const page = `/liqmap/${sym.replace(/USDT$/, "").toLowerCase()}`;
|
|
915
|
-
|
|
1182
|
+
const full = { ...data, source: `${PUBLIC_URL}${page}`, provenance: provenanceFor(data, path, page) };
|
|
1183
|
+
if (detail)
|
|
1184
|
+
return ok(full);
|
|
1185
|
+
const N = 30;
|
|
1186
|
+
return ok(trimmedAnswer(full, (rec, t) => {
|
|
1187
|
+
if (isRec(rec.real_levels) && Array.isArray(rec.real_levels.levels) && rec.real_levels.levels.length > N) {
|
|
1188
|
+
/* The levels nearest the price, kept in the route's price order. */
|
|
1189
|
+
const levels = rec.real_levels.levels;
|
|
1190
|
+
const dist = (l) => (isRec(l) && typeof l.distance_pct === "number" ? Math.abs(l.distance_pct) : Infinity);
|
|
1191
|
+
const nearest = new Set([...levels].sort((a, b) => dist(a) - dist(b)).slice(0, N));
|
|
1192
|
+
rec.real_levels = { ...rec.real_levels, levels: levels.filter((l) => nearest.has(l)) };
|
|
1193
|
+
t.note("real_levels.levels", N, levels.length, "pass detail=true for every level");
|
|
1194
|
+
}
|
|
1195
|
+
if (isRec(rec.real_liquidations)) {
|
|
1196
|
+
/* Newest print first. */
|
|
1197
|
+
rec.real_liquidations = { ...rec.real_liquidations, points: t.head(rec.real_liquidations.points, N, "real_liquidations.points", "pass detail=true for every print") };
|
|
1198
|
+
}
|
|
1199
|
+
}));
|
|
916
1200
|
}
|
|
917
1201
|
catch (err) {
|
|
918
1202
|
return fail(err);
|
|
@@ -920,7 +1204,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
920
1204
|
});
|
|
921
1205
|
server.registerTool("get_tradfi_board", {
|
|
922
1206
|
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.",
|
|
1207
|
+
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
1208
|
inputSchema: {
|
|
925
1209
|
market: z
|
|
926
1210
|
.string()
|
|
@@ -936,9 +1220,10 @@ export function registerByKaranteliTools(server, options) {
|
|
|
936
1220
|
.regex(/^[A-Z0-9]{2,24}$/)
|
|
937
1221
|
.optional()
|
|
938
1222
|
.describe("One Binance TradFi symbol, e.g. TSLAUSDT"),
|
|
1223
|
+
limit: z.coerce.number().int().min(1).max(150).optional().describe("Optional. Contracts without symbol, largest open interest first (default 30)."),
|
|
939
1224
|
},
|
|
940
1225
|
annotations: READ_ONLY,
|
|
941
|
-
}, async ({ market, symbol }) => {
|
|
1226
|
+
}, async ({ market, symbol, limit }) => {
|
|
942
1227
|
try {
|
|
943
1228
|
const params = new URLSearchParams();
|
|
944
1229
|
if (market)
|
|
@@ -947,7 +1232,12 @@ export function registerByKaranteliTools(server, options) {
|
|
|
947
1232
|
params.set("symbol", symbol);
|
|
948
1233
|
const qs = params.toString();
|
|
949
1234
|
const path = `/api/public/tradfi${qs ? `?${qs}` : ""}`;
|
|
950
|
-
|
|
1235
|
+
const data = await fetchJson(path);
|
|
1236
|
+
if (symbol)
|
|
1237
|
+
return ok(withProvenance(data, path));
|
|
1238
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1239
|
+
rec.rows = t.head(rec.rows, limit ?? 30, "rows", "pass symbol, market, or limit up to 150");
|
|
1240
|
+
}, "rows"));
|
|
951
1241
|
}
|
|
952
1242
|
catch (err) {
|
|
953
1243
|
return fail(err);
|
|
@@ -955,14 +1245,16 @@ export function registerByKaranteliTools(server, options) {
|
|
|
955
1245
|
});
|
|
956
1246
|
server.registerTool("get_rsi_heatmap", {
|
|
957
1247
|
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.",
|
|
1248
|
+
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
1249
|
inputSchema: {
|
|
960
1250
|
symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,24}$/).optional().describe("One Binance symbol, e.g. BTCUSDT"),
|
|
961
1251
|
kind: z.enum(["crypto", "tradfi"]).optional().describe("crypto | tradfi"),
|
|
962
1252
|
sort: z.enum(["15m", "1h", "4h", "12h", "1d", "3d", "1w", "1M"]).optional().describe("Interval to sort by, descending"),
|
|
1253
|
+
order: z.enum(["desc", "asc"]).optional().describe("Optional. With sort: desc (default, most overbought first) or asc (most oversold first)."),
|
|
1254
|
+
limit: z.coerce.number().int().min(1).max(400).optional().describe("Optional. Rows without symbol (default 30)."),
|
|
963
1255
|
},
|
|
964
1256
|
annotations: READ_ONLY,
|
|
965
|
-
}, async ({ symbol, kind, sort }) => {
|
|
1257
|
+
}, async ({ symbol, kind, sort, order, limit }) => {
|
|
966
1258
|
try {
|
|
967
1259
|
const params = new URLSearchParams();
|
|
968
1260
|
if (symbol)
|
|
@@ -973,7 +1265,18 @@ export function registerByKaranteliTools(server, options) {
|
|
|
973
1265
|
params.set("sort", sort);
|
|
974
1266
|
const qs = params.toString();
|
|
975
1267
|
const path = `/api/public/rsi${qs ? `?${qs}` : ""}`;
|
|
976
|
-
|
|
1268
|
+
const data = await fetchJson(path);
|
|
1269
|
+
if (symbol)
|
|
1270
|
+
return ok(withProvenance(data, path));
|
|
1271
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1272
|
+
let rows = list(rec.rows);
|
|
1273
|
+
if (sort && order === "asc") {
|
|
1274
|
+
/* The route sorts descending; reversed, rows without a reading on that interval stay last. */
|
|
1275
|
+
const has = (r) => isRec(r) && isRec(r.rsi) && typeof r.rsi[sort] === "number";
|
|
1276
|
+
rows = [...rows.filter(has).reverse(), ...rows.filter((r) => !has(r))];
|
|
1277
|
+
}
|
|
1278
|
+
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)");
|
|
1279
|
+
}, "rows"));
|
|
977
1280
|
}
|
|
978
1281
|
catch (err) {
|
|
979
1282
|
return fail(err);
|
|
@@ -998,13 +1301,14 @@ export function registerByKaranteliTools(server, options) {
|
|
|
998
1301
|
});
|
|
999
1302
|
server.registerTool("get_hl_whales", {
|
|
1000
1303
|
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.",
|
|
1304
|
+
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
1305
|
inputSchema: {
|
|
1003
1306
|
coin: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{1,24}$/).optional().describe("One coin, e.g. BTC"),
|
|
1004
1307
|
events: z.boolean().optional().describe("Include the last 200 position change events"),
|
|
1308
|
+
limit: z.coerce.number().int().min(1).max(200).optional().describe("Optional. Coins and positions without coin, largest first (default 30)."),
|
|
1005
1309
|
},
|
|
1006
1310
|
annotations: READ_ONLY,
|
|
1007
|
-
}, async ({ coin, events }) => {
|
|
1311
|
+
}, async ({ coin, events, limit }) => {
|
|
1008
1312
|
try {
|
|
1009
1313
|
const params = new URLSearchParams();
|
|
1010
1314
|
if (coin)
|
|
@@ -1013,7 +1317,13 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1013
1317
|
params.set("events", "1");
|
|
1014
1318
|
const qs = params.toString();
|
|
1015
1319
|
const path = `/api/public/hyperliquid-whales${qs ? `?${qs}` : ""}`;
|
|
1016
|
-
|
|
1320
|
+
const data = await fetchJson(path);
|
|
1321
|
+
if (coin)
|
|
1322
|
+
return ok(withProvenance(data, path));
|
|
1323
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1324
|
+
rec.by_coin = t.head(rec.by_coin, limit ?? 30, "by_coin", "pass coin, or limit up to 200");
|
|
1325
|
+
rec.positions = t.head(rec.positions, limit ?? 30, "positions", "pass coin, or limit up to 200");
|
|
1326
|
+
}, "positions"));
|
|
1017
1327
|
}
|
|
1018
1328
|
catch (err) {
|
|
1019
1329
|
return fail(err);
|
|
@@ -1021,15 +1331,47 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1021
1331
|
});
|
|
1022
1332
|
server.registerTool("get_positioning", {
|
|
1023
1333
|
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.",
|
|
1334
|
+
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
1335
|
inputSchema: {
|
|
1026
1336
|
symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,24}$/).optional().describe("One Binance symbol, e.g. BTCUSDT"),
|
|
1337
|
+
limit: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Perps without symbol, most traded first (default 10, up to 30)."),
|
|
1338
|
+
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
1339
|
},
|
|
1028
1340
|
annotations: READ_ONLY,
|
|
1029
|
-
}, async ({ symbol }) => {
|
|
1341
|
+
}, async ({ symbol, limit, cvd_points }) => {
|
|
1030
1342
|
try {
|
|
1031
1343
|
const path = `/api/public/positioning${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ""}`;
|
|
1032
|
-
|
|
1344
|
+
const data = await fetchJson(path);
|
|
1345
|
+
const points = cvd_points ?? (symbol ? undefined : 24);
|
|
1346
|
+
if (symbol && points === undefined)
|
|
1347
|
+
return ok(withProvenance(data, path));
|
|
1348
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1349
|
+
if (!symbol)
|
|
1350
|
+
rec.rows = t.head(rec.rows, limit ?? 10, "rows", "pass symbol, or limit up to 30");
|
|
1351
|
+
if (points !== undefined && isRec(rec.cvd)) {
|
|
1352
|
+
const kept = {};
|
|
1353
|
+
const total = {};
|
|
1354
|
+
const cvd = {};
|
|
1355
|
+
for (const [sym, intervals] of Object.entries(rec.cvd)) {
|
|
1356
|
+
if (!isRec(intervals)) {
|
|
1357
|
+
cvd[sym] = intervals;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
const out = {};
|
|
1361
|
+
for (const [interval, series] of Object.entries(intervals)) {
|
|
1362
|
+
const all = list(series);
|
|
1363
|
+
total[interval] = (total[interval] ?? 0) + all.length;
|
|
1364
|
+
kept[interval] = (kept[interval] ?? 0) + Math.min(all.length, points);
|
|
1365
|
+
/* Oldest point first, so the newest are the tail. */
|
|
1366
|
+
out[interval] = all.length > points ? all.slice(-points) : series;
|
|
1367
|
+
}
|
|
1368
|
+
cvd[sym] = out;
|
|
1369
|
+
}
|
|
1370
|
+
rec.cvd = cvd;
|
|
1371
|
+
for (const interval of Object.keys(total))
|
|
1372
|
+
t.note(`cvd.*.${interval}`, kept[interval], total[interval], "pass symbol for its full CVD, or cvd_points up to 288");
|
|
1373
|
+
}
|
|
1374
|
+
}, symbol ? undefined : "rows"));
|
|
1033
1375
|
}
|
|
1034
1376
|
catch (err) {
|
|
1035
1377
|
return fail(err);
|
|
@@ -1037,13 +1379,14 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1037
1379
|
});
|
|
1038
1380
|
server.registerTool("get_turkey_premium", {
|
|
1039
1381
|
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.",
|
|
1382
|
+
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
1383
|
inputSchema: {
|
|
1042
1384
|
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
1385
|
history_days: z.number().int().min(1).max(30).optional().describe("Include 15-minute index history for the pair, 1..30 days"),
|
|
1386
|
+
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
1387
|
},
|
|
1045
1388
|
annotations: READ_ONLY,
|
|
1046
|
-
}, async ({ pair, history_days }) => {
|
|
1389
|
+
}, async ({ pair, history_days, daily_days }) => {
|
|
1047
1390
|
try {
|
|
1048
1391
|
const q = new URLSearchParams();
|
|
1049
1392
|
if (pair)
|
|
@@ -1052,7 +1395,17 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1052
1395
|
q.set("days", String(history_days));
|
|
1053
1396
|
const qs = q.toString();
|
|
1054
1397
|
const path = `/api/public/turkey-premium${qs ? `?${qs}` : ""}`;
|
|
1055
|
-
|
|
1398
|
+
const data = await fetchJson(path);
|
|
1399
|
+
const wantDays = daily_days ?? (pair ? undefined : 7);
|
|
1400
|
+
if (wantDays === undefined)
|
|
1401
|
+
return ok(withProvenance(data, path));
|
|
1402
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1403
|
+
const daily = list(rec.daily);
|
|
1404
|
+
const keep = newestDates(daily, wantDays);
|
|
1405
|
+
const kept = daily.filter((r) => isRec(r) && keep.has(String(r.date)));
|
|
1406
|
+
t.note("daily", kept.length, daily.length, "pass pair, or daily_days up to 30");
|
|
1407
|
+
rec.daily = kept;
|
|
1408
|
+
}));
|
|
1056
1409
|
}
|
|
1057
1410
|
catch (err) {
|
|
1058
1411
|
return fail(err);
|
|
@@ -1135,12 +1488,12 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1135
1488
|
});
|
|
1136
1489
|
server.registerTool("get_options_chain", {
|
|
1137
1490
|
title: "Options chain hour by hour: open interest and IV per expiry and strike, the change over 1h and 24h, the ATM IV path",
|
|
1138
|
-
description: "Call this when the user asks how the BTC or ETH option chain moved today or over the last day: open interest and mark IV per expiry and strike from ByKaranteli's own hourly capture of every listed venue, the change over the last hour and the last 24 hours, and the front expiry's ATM IV hour by hour.
|
|
1491
|
+
description: "Call this when the user asks how the BTC or ETH option chain moved today or over the last day: open interest and mark IV per expiry and strike from ByKaranteli's own hourly capture of every listed venue, the change over the last hour and the last 24 hours, and the front expiry's ATM IV hour by hour. The Free plan lists the largest strikes; Builder and higher plans list every strike. Recorded from 2026-09-30, so the first days carry a short history.",
|
|
1139
1492
|
inputSchema: {
|
|
1140
1493
|
currency: z.enum(["BTC", "ETH"]).optional().describe("BTC or ETH (default BTC)"),
|
|
1141
1494
|
venue: z.enum(["all", "deribit", "bybit", "binance", "okx", "delta"]).optional().describe("One venue or all (default all)"),
|
|
1142
1495
|
expiry: z.string().trim().toUpperCase().regex(/^\d{1,2}[A-Z]{3}\d{2}$/).optional().describe("Expiry as DDMONYY, e.g. 31OCT26 (default: every expiry)"),
|
|
1143
|
-
strikes: z.enum(["top", "all"]).optional().describe("top (largest strikes) or all (
|
|
1496
|
+
strikes: z.enum(["top", "all"]).optional().describe("top (largest strikes) or all (Builder and higher plans)"),
|
|
1144
1497
|
},
|
|
1145
1498
|
annotations: READ_ONLY,
|
|
1146
1499
|
}, async ({ currency, venue, expiry, strikes }) => {
|
|
@@ -1207,13 +1560,14 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1207
1560
|
});
|
|
1208
1561
|
server.registerTool("get_tradfi_gaps", {
|
|
1209
1562
|
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.",
|
|
1563
|
+
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
1564
|
inputSchema: {
|
|
1212
1565
|
window: z.enum(["weekend", "night"]).optional().describe("weekend (Friday close to Monday open) or night (cash close to next open); default weekend"),
|
|
1213
1566
|
symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9.]{1,24}$/).optional().describe("One underlying, e.g. TSLA or XAU (default: the whole board)"),
|
|
1567
|
+
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
1568
|
},
|
|
1215
1569
|
annotations: READ_ONLY,
|
|
1216
|
-
}, async ({ window, symbol }) => {
|
|
1570
|
+
}, async ({ window, symbol, limit }) => {
|
|
1217
1571
|
try {
|
|
1218
1572
|
const q = new URLSearchParams();
|
|
1219
1573
|
if (window !== undefined)
|
|
@@ -1222,7 +1576,15 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1222
1576
|
q.set("symbol", String(symbol));
|
|
1223
1577
|
const qs = q.toString();
|
|
1224
1578
|
const path = `/api/public/tradfi-gaps${qs ? `?${qs}` : ""}`;
|
|
1225
|
-
|
|
1579
|
+
const data = await fetchJson(path);
|
|
1580
|
+
if (symbol !== undefined)
|
|
1581
|
+
return ok(withProvenance(data, path));
|
|
1582
|
+
const n = limit ?? 15;
|
|
1583
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1584
|
+
rec.summary = t.head(rec.summary, n, "summary", "pass symbol for every window of one underlying, or limit up to 60");
|
|
1585
|
+
if (isRec(rec.latest))
|
|
1586
|
+
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") };
|
|
1587
|
+
}, "summary"));
|
|
1226
1588
|
}
|
|
1227
1589
|
catch (err) {
|
|
1228
1590
|
return fail(err);
|
|
@@ -1230,16 +1592,17 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1230
1592
|
});
|
|
1231
1593
|
server.registerTool("get_solana_perps", {
|
|
1232
1594
|
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
|
|
1595
|
+
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 on the Free plan, 30 on Builder and higher plans; Jupiter history is get_jupiter_perps). Without venue, the 30 largest markets; pass venue, or limit (up to 300) for more.",
|
|
1234
1596
|
inputSchema: {
|
|
1235
1597
|
venue: z.string().trim().toLowerCase().regex(/^(jupiter|pacifica|phoenix|gmtrade|velocity|bullet)$/).optional().describe("Venue filter: jupiter, pacifica, phoenix, gmtrade, velocity or bullet"),
|
|
1236
1598
|
/* Passed as typed: Pacifica and Phoenix list mixed-case markets (kBONK, kPEPE, kSHIB) and the route matches the
|
|
1237
1599
|
symbol case-insensitively, so upper-casing here would only change what the caller sees echoed back. */
|
|
1238
1600
|
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
1601
|
history_days: z.number().int().min(1).max(30).optional().describe("Hourly history for the symbol, 1..30 days"),
|
|
1602
|
+
limit: z.coerce.number().int().min(1).max(300).optional().describe("Optional. Markets without venue, largest first (default 30)."),
|
|
1240
1603
|
},
|
|
1241
1604
|
annotations: READ_ONLY,
|
|
1242
|
-
}, async ({ venue, symbol, history_days }) => {
|
|
1605
|
+
}, async ({ venue, symbol, history_days, limit }) => {
|
|
1243
1606
|
try {
|
|
1244
1607
|
const q = new URLSearchParams();
|
|
1245
1608
|
if (venue)
|
|
@@ -1250,7 +1613,12 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1250
1613
|
q.set("days", String(history_days));
|
|
1251
1614
|
const qs = q.toString();
|
|
1252
1615
|
const path = `/api/public/solana-perps${qs ? `?${qs}` : ""}`;
|
|
1253
|
-
|
|
1616
|
+
const data = await fetchJson(path);
|
|
1617
|
+
if (venue)
|
|
1618
|
+
return ok(withProvenance(data, path));
|
|
1619
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1620
|
+
rec.markets = t.head(rec.markets, limit ?? 30, "markets", "pass venue for every market of one venue, or limit up to 300");
|
|
1621
|
+
}, "markets"));
|
|
1254
1622
|
}
|
|
1255
1623
|
catch (err) {
|
|
1256
1624
|
return fail(err);
|
|
@@ -1258,13 +1626,38 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1258
1626
|
});
|
|
1259
1627
|
server.registerTool("get_coverage", {
|
|
1260
1628
|
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: {
|
|
1629
|
+
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.",
|
|
1630
|
+
inputSchema: {
|
|
1631
|
+
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)."),
|
|
1632
|
+
},
|
|
1263
1633
|
annotations: READ_ONLY,
|
|
1264
|
-
}, async () => {
|
|
1634
|
+
}, async ({ detail }) => {
|
|
1265
1635
|
try {
|
|
1266
1636
|
const path = "/api/public/coverage";
|
|
1267
|
-
|
|
1637
|
+
const data = await fetchJson(path);
|
|
1638
|
+
if (detail)
|
|
1639
|
+
return ok(withProvenance(data, path));
|
|
1640
|
+
const more = "pass detail=true for the full registry";
|
|
1641
|
+
return ok(compact(data, path, (rec, t) => {
|
|
1642
|
+
rec.snapshots = t.head(rec.snapshots, 30, "snapshots", more);
|
|
1643
|
+
const liq = list(rec.liquidations);
|
|
1644
|
+
let translated = 0;
|
|
1645
|
+
rec.liquidations = liq.map((r) => {
|
|
1646
|
+
if (!isRec(r))
|
|
1647
|
+
return r;
|
|
1648
|
+
/* note_en stays; the zh, ru and ko notes translate it for the site's other locales. */
|
|
1649
|
+
const { note_zh, note_ru, note_ko, ...rest } = r;
|
|
1650
|
+
if (note_zh !== undefined || note_ru !== undefined || note_ko !== undefined)
|
|
1651
|
+
translated += 1;
|
|
1652
|
+
return rest;
|
|
1653
|
+
});
|
|
1654
|
+
t.note("liquidations[].note_zh, note_ru, note_ko", 0, translated, more);
|
|
1655
|
+
if (isRec(rec.matrix) && isRec(rec.matrix.cells)) {
|
|
1656
|
+
const products = Object.keys(rec.matrix.cells).length;
|
|
1657
|
+
rec.matrix = { ...rec.matrix, cells: {} };
|
|
1658
|
+
t.note("matrix.cells", 0, products, `${more}; matrix.products already lists each product's venues`);
|
|
1659
|
+
}
|
|
1660
|
+
}));
|
|
1268
1661
|
}
|
|
1269
1662
|
catch (err) {
|
|
1270
1663
|
return fail(err);
|
|
@@ -1390,7 +1783,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1390
1783
|
/* ---- 0.31.0: recorded series, alert text, alert recipes, watchlists ---- */
|
|
1391
1784
|
server.registerTool("get_series", {
|
|
1392
1785
|
title: "Recorded series: bars or points of one metric for one perpetual (price, OI, funding, CVD, liquidations, long/short, RSI and more)",
|
|
1393
|
-
description: `Call this when the user wants the history of one recorded metric for one perpetual as a time series: price candles, volume, perp or spot CVD, open interest, funding (one venue, or OI-weighted across every venue as funding_agg), liquidations, long/short ratios, RSI, the Coinbase premium, US spot ETF flows, borrow rates or Hyperliquid whale net flow, for charting, backtesting or "what did X do over the last N days". Returns the /api/series answer (points as [t, v], or [t, o, h, l, c, v] for price, with unit, kind, source and source_kind, the bars served and whether member depth applied) plus provenance. metric is one of: ${SERIES_METRIC_KEYS.join(", ")} (unit, finest period and venue support of each: https://bykaranteli.com/api/series/metrics).
|
|
1786
|
+
description: `Call this when the user wants the history of one recorded metric for one perpetual as a time series: price candles, volume, perp or spot CVD, open interest, funding (one venue, or OI-weighted across every venue as funding_agg), liquidations, long/short ratios, RSI, the Coinbase premium, US spot ETF flows, borrow rates or Hyperliquid whale net flow, for charting, backtesting or "what did X do over the last N days". Returns the /api/series answer (points as [t, v], or [t, o, h, l, c, v] for price, with unit, kind, source and source_kind, the bars served and whether member depth applied) plus provenance. metric is one of: ${SERIES_METRIC_KEYS.join(", ")} (unit, finest period and venue support of each: https://bykaranteli.com/api/series/metrics). The Free plan gets public depth: fewer bars and no bars under 15 minutes (1m, 3m, 5m, 10m); Builder and higher plans get member depth; the bar limits are in the same list. The answer names the venue it was read from (venue) and every venue a market-wide series sums (venues). Without from or limit, the newest 300 bars; pass from and to (ISO) for a window, or limit for more of the newest bars.`,
|
|
1394
1787
|
inputSchema: {
|
|
1395
1788
|
metric: z.string().describe("string, metric key, e.g. price, oi, funding, liquidations (list: /api/series/metrics)"),
|
|
1396
1789
|
symbol: z.string().optional().describe("string, optional Binance USDT-M perp or coin, e.g. BTCUSDT or BTC (default BTCUSDT)"),
|
|
@@ -1398,7 +1791,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1398
1791
|
venue: z.string().optional().describe("string, optional venue id for price, oi, funding or borrow, e.g. okx (default binance)"),
|
|
1399
1792
|
from: z.string().optional().describe("string, optional ISO start, e.g. 2026-09-01T00:00:00Z"),
|
|
1400
1793
|
to: z.string().optional().describe("string, optional ISO end (default now)"),
|
|
1401
|
-
limit: z.number().optional().describe(`number, optional newest bars, 10..${SERIES_MEMBER_BAR_LIMIT}; the
|
|
1794
|
+
limit: z.number().optional().describe(`number, optional newest bars, 10..${SERIES_MEMBER_BAR_LIMIT}; the plan's depth caps it (default 300 without from)`),
|
|
1402
1795
|
},
|
|
1403
1796
|
annotations: READ_ONLY,
|
|
1404
1797
|
}, async (args = {}) => {
|
|
@@ -1420,6 +1813,9 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1420
1813
|
const bars = from ? Math.ceil((end.getTime() - from.getTime()) / PERIOD_MS[period]) : undefined;
|
|
1421
1814
|
if (bars !== undefined)
|
|
1422
1815
|
limit = Math.min(limit ?? bars, bars);
|
|
1816
|
+
/* 0.35.1: the newest 300 bars when neither from nor limit is given (the route gives members 1000 by default). */
|
|
1817
|
+
if (limit === undefined)
|
|
1818
|
+
limit = 300;
|
|
1423
1819
|
if (limit !== undefined)
|
|
1424
1820
|
limit = Math.max(10, Math.min(SERIES_MEMBER_BAR_LIMIT, limit));
|
|
1425
1821
|
const q = new URLSearchParams({ metric, symbol, period });
|
|
@@ -1450,7 +1846,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1450
1846
|
});
|
|
1451
1847
|
server.registerTool("list_alert_recipes", {
|
|
1452
1848
|
title: "Your alert recipes: every alert on your account with its conditions, scope, channels and when it last fired",
|
|
1453
|
-
description: "Call this when the user asks which alerts they have, whether an alert fired, or before deleting or changing one. Returns the account's alert recipes (id, name, scope, symbols, conditions, cooldown in hours, enabled, last fired, fire count, channels). Needs
|
|
1849
|
+
description: "Call this when the user asks which alerts they have, whether an alert fired, or before deleting or changing one. Returns the account's alert recipes (id, name, scope, symbols, conditions, cooldown in hours, enabled, last fired, fire count, channels). Needs a signed-in account (OAuth sign-in or account key); reads nothing but this account.",
|
|
1454
1850
|
inputSchema: {},
|
|
1455
1851
|
annotations: ACCOUNT_READ,
|
|
1456
1852
|
}, async () => {
|
|
@@ -1459,7 +1855,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1459
1855
|
});
|
|
1460
1856
|
server.registerTool("create_alert_recipe", {
|
|
1461
1857
|
title: "Create an alert recipe (writes to your account)",
|
|
1462
|
-
description: `Call this when the user asks to be alerted when a metric crosses a threshold and has agreed to the exact condition (parse_alert_text turns their words into one). Saves an alert recipe on the account: it fires when every condition holds, delivers by Telegram first with email as fallback unless channels says otherwise, and waits cooldown_hours before it fires again for the same symbol. field is one of: ${RECIPE_FIELD_KEYS.join(", ")}; op is one of ${RECIPE_OPS.join(" ")}; percent fields take percent values (0.05 means 0.05%). Pass field, op and threshold for one condition, or conditions for up to ${RECIPE_LIMITS.MAX_CONDITIONS}. scope: watchlist (default), any, or symbols with symbols. The plan sets how many recipes an account keeps; past it the tool answers with the limit. Writes are rate limited per key and each one is recorded on the account.`,
|
|
1858
|
+
description: `Call this when the user asks to be alerted when a metric crosses a threshold and has agreed to the exact condition (parse_alert_text turns their words into one). Saves an alert recipe on the account: it fires when every condition holds, delivers by Telegram first with email as fallback unless channels says otherwise, and waits cooldown_hours before it fires again for the same symbol. field is one of: ${RECIPE_FIELD_KEYS.join(", ")}; op is one of ${RECIPE_OPS.join(" ")}; percent fields take percent values (0.05 means 0.05%). Pass field, op and threshold for one condition, or conditions for up to ${RECIPE_LIMITS.MAX_CONDITIONS}. scope: watchlist (default), any, or symbols with symbols. The plan sets how many recipes an account keeps; past it the tool answers with the limit. Writes are rate limited per connection (sign-in or key) and each one is recorded on the account.`,
|
|
1463
1859
|
inputSchema: {
|
|
1464
1860
|
field: z.string().optional().describe("string, one condition's metric, e.g. funding_rate_pct"),
|
|
1465
1861
|
op: z.enum(RECIPE_OPS).optional().describe(`string, one condition's comparison: ${RECIPE_OPS.join(" | ")}`),
|
|
@@ -1521,7 +1917,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1521
1917
|
});
|
|
1522
1918
|
server.registerTool("delete_alert_recipe", {
|
|
1523
1919
|
title: "Delete one of your alert recipes (writes to your account)",
|
|
1524
|
-
description: "Call this when the user asks to remove an alert. Deletes the alert recipe with this id from the account (list_alert_recipes shows the ids); an id that is not one of the account's recipes changes nothing and says so. Writes are rate limited per key and each one is recorded on the account.",
|
|
1920
|
+
description: "Call this when the user asks to remove an alert. Deletes the alert recipe with this id from the account (list_alert_recipes shows the ids); an id that is not one of the account's recipes changes nothing and says so. Writes are rate limited per connection (sign-in or key) and each one is recorded on the account.",
|
|
1525
1921
|
inputSchema: {
|
|
1526
1922
|
id: z.string().describe("string, the recipe id from list_alert_recipes"),
|
|
1527
1923
|
},
|
|
@@ -1539,7 +1935,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1539
1935
|
});
|
|
1540
1936
|
server.registerTool("list_watchlists", {
|
|
1541
1937
|
title: "Your watchlists and the symbols on each",
|
|
1542
|
-
description: "Call this when the user asks what is on their watchlist, which lists they have, or before adding or removing a symbol. Returns the account's watchlists (id, name, symbol count) and, unless include_symbols is false, the symbols on each list in order. Needs
|
|
1938
|
+
description: "Call this when the user asks what is on their watchlist, which lists they have, or before adding or removing a symbol. Returns the account's watchlists (id, name, symbol count) and, unless include_symbols is false, the symbols on each list in order. Needs a signed-in account (OAuth sign-in or account key); reads nothing but this account.",
|
|
1543
1939
|
inputSchema: {
|
|
1544
1940
|
include_symbols: z.boolean().optional().describe("boolean, optional: include each list's symbols (default true)"),
|
|
1545
1941
|
},
|
|
@@ -1562,7 +1958,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1562
1958
|
});
|
|
1563
1959
|
server.registerTool("add_watchlist_symbol", {
|
|
1564
1960
|
title: "Add a symbol to your watchlist (writes to your account)",
|
|
1565
|
-
description: "Call this when the user asks to watch, follow or add a coin to their watchlist. Adds the symbol to the list named by watchlist_id, else to the default list (or the only list the account has); a symbol already on the list stays once. Returns the list's symbols after the change. Writes are rate limited per key and each one is recorded on the account.",
|
|
1961
|
+
description: "Call this when the user asks to watch, follow or add a coin to their watchlist. Adds the symbol to the list named by watchlist_id, else to the default list (or the only list the account has); a symbol already on the list stays once. Returns the list's symbols after the change. Writes are rate limited per connection (sign-in or key) and each one is recorded on the account.",
|
|
1566
1962
|
inputSchema: {
|
|
1567
1963
|
symbol: z.string().describe("string, a Binance USDT-M perp or coin, e.g. SOLUSDT or SOL"),
|
|
1568
1964
|
watchlist_id: z.string().optional().describe("string, optional list id from list_watchlists (default: the default list)"),
|
|
@@ -1582,7 +1978,7 @@ export function registerByKaranteliTools(server, options) {
|
|
|
1582
1978
|
});
|
|
1583
1979
|
server.registerTool("remove_watchlist_symbol", {
|
|
1584
1980
|
title: "Remove a symbol from your watchlist (writes to your account)",
|
|
1585
|
-
description: "Call this when the user asks to stop watching a coin or remove it from their watchlist. Removes the symbol from the list named by watchlist_id, else from the default list (or the only list the account has); a symbol that is not on the list changes nothing. Returns the list's symbols after the change. Writes are rate limited per key and each one is recorded on the account.",
|
|
1981
|
+
description: "Call this when the user asks to stop watching a coin or remove it from their watchlist. Removes the symbol from the list named by watchlist_id, else from the default list (or the only list the account has); a symbol that is not on the list changes nothing. Returns the list's symbols after the change. Writes are rate limited per connection (sign-in or key) and each one is recorded on the account.",
|
|
1586
1982
|
inputSchema: {
|
|
1587
1983
|
symbol: z.string().describe("string, the symbol to remove, e.g. SOLUSDT or SOL"),
|
|
1588
1984
|
watchlist_id: z.string().optional().describe("string, optional list id from list_watchlists (default: the default list)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bykaranteli-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
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": {
|