bykaranteli-mcp 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/compact.d.ts +105 -0
- package/dist/compact.js +159 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +209 -74
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
## [1.1.0] - 2026-10-24
|
|
6
|
+
|
|
7
|
+
68 tools.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `detail` on thirteen large tools (get_liqmap, get_turkey_premium, get_correlations, get_hl_whales, get_options_flow, get_funding_arbitrage, get_desk_notes, get_positioning, get_coverage, get_new_listings, get_venue_markets, get_tokenized_stocks, get_factor_board): `"compact"` answers a shorter page than the default one (fewer rows, numbers rounded to six significant digits, long free text cut, the LiqMap heatmap grid and axes left out, each pair of the correlation matrix once for BTC and ETH), `"full"` the whole route answer (still under the 100,000 character ceiling of every answer), omitted the default first page. Every cut is named in `trimmed` as before. get_liqmap and get_coverage keep their boolean form, where `true` means full. Measured against the live routes on 24 Oct 2026: every compact answer under 25,000 characters.
|
|
12
|
+
- `compact` entry (`bykaranteli-mcp/compact`): `compactValue`, `compactToolText` and the per-tool `COMPACT_RULES`, the code the compact pages use, exported for hosts that run the tools in process and cap an answer's size themselves (the AI desk on bykaranteli.com).
|
|
13
|
+
- `headersProvider` in the register options: extra request headers read per outbound fetch, for a host that marks member depth for the caller it answers for. Never a credential.
|
|
14
|
+
|
|
5
15
|
## [1.0.1] - 2026-10-23
|
|
6
16
|
|
|
7
17
|
68 tools.
|
package/README.md
CHANGED
|
@@ -129,6 +129,8 @@ All market answers are JSON and carry a `generatedAt` timestamp, a `source` URL
|
|
|
129
129
|
|
|
130
130
|
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.
|
|
131
131
|
|
|
132
|
+
Thirteen large tools (get_liqmap, get_turkey_premium, get_correlations, get_hl_whales, get_options_flow, get_funding_arbitrage, get_desk_notes, get_positioning, get_coverage, get_new_listings, get_venue_markets, get_tokenized_stocks, get_factor_board) also take `detail`: `"compact"` answers a shorter page than the default one (fewer rows, numbers rounded to six significant digits, long free text cut, grids left out; every cut named in `trimmed`), `"full"` the whole route answer. get_liqmap and get_coverage keep their boolean form, where `true` means full. Since 1.1.0.
|
|
133
|
+
|
|
132
134
|
## Account tools (alerts and watchlist)
|
|
133
135
|
|
|
134
136
|
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.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact answers (1.1.0, 2026-10-24). Two callers:
|
|
3
|
+
* - the `detail: "compact"` value of thirteen large tools (tools.ts): a shorter page than the default one, for a
|
|
4
|
+
* model that only needs the headline figures;
|
|
5
|
+
* - the AI desk on bykaranteli.com, which runs the same tools in process and caps every tool answer at a fixed
|
|
6
|
+
* number of characters before the model reads it (compactToolText).
|
|
7
|
+
* Nothing here recomputes a figure: lists are cut in the route's own order, numbers are rounded to a few significant
|
|
8
|
+
* digits, long free text is shortened, and every cut is named in `trimmed` the way the default pages name theirs.
|
|
9
|
+
*/
|
|
10
|
+
export type CompactCut = {
|
|
11
|
+
field: string;
|
|
12
|
+
returned: number;
|
|
13
|
+
total: number;
|
|
14
|
+
more: string;
|
|
15
|
+
};
|
|
16
|
+
export type CompactRule = {
|
|
17
|
+
/** Items kept per list (default 10; Infinity leaves every list as the tool cut it). The thirteen tool rules leave
|
|
18
|
+
* lists alone: each tool cuts its own lists in its handler, where it knows which end of a series is the newest. */
|
|
19
|
+
maxItems?: number;
|
|
20
|
+
/** Per-key item caps by the list's own key, e.g. { exchanges: 3 }; wins over maxItems for that key. */
|
|
21
|
+
fields?: Readonly<Record<string, number>>;
|
|
22
|
+
/** Top-level keys left out entirely (heatmap grids, label axes). */
|
|
23
|
+
drop?: readonly string[];
|
|
24
|
+
/** Keys whose strings are never shortened and whose lists are never cut (the quotable text of a desk note, the
|
|
25
|
+
* routes' own field definitions in `note`). */
|
|
26
|
+
keepKeys?: readonly string[];
|
|
27
|
+
/** Longest string kept (default 300 characters); longer ones are cut with a marker. */
|
|
28
|
+
maxString?: number;
|
|
29
|
+
/** Significant digits kept on non-integer numbers (default 6). */
|
|
30
|
+
significant?: number;
|
|
31
|
+
/** The `more` text of every cut (default: the full answer parameter). */
|
|
32
|
+
more?: string;
|
|
33
|
+
};
|
|
34
|
+
/** Walks one answer; returns the shortened copy and the cuts it made. */
|
|
35
|
+
export declare function compactValue(value: unknown, rule?: CompactRule): {
|
|
36
|
+
value: unknown;
|
|
37
|
+
cuts: CompactCut[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* One tool answer (its JSON text) under a character cap. The caps shrink until the text fits: first the rule's own
|
|
41
|
+
* item caps, then halves of them down to two items, then the largest top-level lists are left out. The answer is
|
|
42
|
+
* never cut mid-JSON; a model always reads a complete document with its `trimmed` notes. Non-JSON text (an error
|
|
43
|
+
* line) is cut at the cap with a marker.
|
|
44
|
+
*/
|
|
45
|
+
export declare function compactToolText(text: string, cap: number, rule?: CompactRule): {
|
|
46
|
+
text: string;
|
|
47
|
+
cuts: CompactCut[];
|
|
48
|
+
capped: boolean;
|
|
49
|
+
chars: number;
|
|
50
|
+
};
|
|
51
|
+
export declare const COMPACT_RULES: {
|
|
52
|
+
readonly get_liqmap: {
|
|
53
|
+
readonly maxItems: number;
|
|
54
|
+
readonly drop: readonly ["heatmap_cells", "x_labels", "y_labels", "band_prices", "price_path", "candles"];
|
|
55
|
+
readonly more: "pass detail \"full\" for every level and print";
|
|
56
|
+
};
|
|
57
|
+
readonly get_turkey_premium: {
|
|
58
|
+
readonly maxItems: number;
|
|
59
|
+
readonly maxString: 300;
|
|
60
|
+
};
|
|
61
|
+
readonly get_correlations: {
|
|
62
|
+
readonly maxItems: number;
|
|
63
|
+
};
|
|
64
|
+
readonly get_hl_whales: {
|
|
65
|
+
readonly maxItems: number;
|
|
66
|
+
};
|
|
67
|
+
readonly get_options_flow: {
|
|
68
|
+
readonly maxItems: number;
|
|
69
|
+
readonly maxString: 300;
|
|
70
|
+
};
|
|
71
|
+
readonly get_funding_arbitrage: {
|
|
72
|
+
readonly maxItems: number;
|
|
73
|
+
};
|
|
74
|
+
readonly get_desk_notes: {
|
|
75
|
+
readonly maxItems: number;
|
|
76
|
+
readonly keepKeys: readonly ["text", "headline", "x_text", "history_line"];
|
|
77
|
+
};
|
|
78
|
+
readonly get_positioning: {
|
|
79
|
+
readonly maxItems: number;
|
|
80
|
+
};
|
|
81
|
+
readonly get_coverage: {
|
|
82
|
+
readonly maxItems: number;
|
|
83
|
+
readonly maxString: 200;
|
|
84
|
+
readonly keepKeys: readonly ["note_en"];
|
|
85
|
+
readonly drop: readonly ["matrix"];
|
|
86
|
+
readonly more: "omit detail for the venue by product matrix";
|
|
87
|
+
};
|
|
88
|
+
readonly get_new_listings: {
|
|
89
|
+
readonly maxItems: number;
|
|
90
|
+
};
|
|
91
|
+
readonly get_venue_markets: {
|
|
92
|
+
readonly maxItems: number;
|
|
93
|
+
};
|
|
94
|
+
readonly get_tokenized_stocks: {
|
|
95
|
+
readonly maxItems: number;
|
|
96
|
+
readonly maxString: 300;
|
|
97
|
+
};
|
|
98
|
+
readonly get_factor_board: {
|
|
99
|
+
readonly maxItems: number;
|
|
100
|
+
readonly maxString: 200;
|
|
101
|
+
readonly keepKeys: readonly ["interpretationEn", "todayText", "bandEn"];
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
export type CompactToolName = keyof typeof COMPACT_RULES;
|
|
105
|
+
export declare const COMPACT_TOOL_NAMES: readonly CompactToolName[];
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact answers (1.1.0, 2026-10-24). Two callers:
|
|
3
|
+
* - the `detail: "compact"` value of thirteen large tools (tools.ts): a shorter page than the default one, for a
|
|
4
|
+
* model that only needs the headline figures;
|
|
5
|
+
* - the AI desk on bykaranteli.com, which runs the same tools in process and caps every tool answer at a fixed
|
|
6
|
+
* number of characters before the model reads it (compactToolText).
|
|
7
|
+
* Nothing here recomputes a figure: lists are cut in the route's own order, numbers are rounded to a few significant
|
|
8
|
+
* digits, long free text is shortened, and every cut is named in `trimmed` the way the default pages name theirs.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_MORE = "narrow the call: a symbol, fewer days or a smaller limit";
|
|
11
|
+
/** Keys never shortened: provenance and page links are what the reader follows. */
|
|
12
|
+
const KEEP_KEYS = new Set(["provenance", "source", "generatedAt", "generated_at", "as_of", "asOf", "trimmed", "license", "page", "links", "url", "destination_url", "image_url", "note"]);
|
|
13
|
+
/** The first `max` UTF-16 units without a dangling high surrogate at the end. */
|
|
14
|
+
function cutString(v, max) {
|
|
15
|
+
let end = max;
|
|
16
|
+
const unit = v.charCodeAt(end - 1);
|
|
17
|
+
if (end > 0 && unit >= 0xd800 && unit <= 0xdbff)
|
|
18
|
+
end -= 1;
|
|
19
|
+
return v.slice(0, end);
|
|
20
|
+
}
|
|
21
|
+
const isRec = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
22
|
+
function roundNumber(n, significant) {
|
|
23
|
+
if (!Number.isFinite(n) || Number.isInteger(n))
|
|
24
|
+
return n;
|
|
25
|
+
const r = Number(n.toPrecision(significant));
|
|
26
|
+
return Number.isFinite(r) ? r : n;
|
|
27
|
+
}
|
|
28
|
+
/** Walks one answer; returns the shortened copy and the cuts it made. */
|
|
29
|
+
export function compactValue(value, rule = {}) {
|
|
30
|
+
const maxItems = rule.maxItems ?? 10;
|
|
31
|
+
const keep = new Set([...KEEP_KEYS, ...(rule.keepKeys ?? [])]);
|
|
32
|
+
const maxString = rule.maxString ?? 300;
|
|
33
|
+
const significant = rule.significant ?? 6;
|
|
34
|
+
const more = rule.more ?? DEFAULT_MORE;
|
|
35
|
+
const cuts = [];
|
|
36
|
+
const noted = new Set();
|
|
37
|
+
const note = (field, returned, total) => {
|
|
38
|
+
if (noted.has(field)) {
|
|
39
|
+
const prev = cuts.find((c) => c.field === field);
|
|
40
|
+
if (prev) {
|
|
41
|
+
prev.returned += returned;
|
|
42
|
+
prev.total += total;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
noted.add(field);
|
|
47
|
+
cuts.push({ field, returned, total, more });
|
|
48
|
+
};
|
|
49
|
+
const walk = (v, path, key, depth) => {
|
|
50
|
+
if (typeof v === "number")
|
|
51
|
+
return roundNumber(v, significant);
|
|
52
|
+
if (typeof v === "string") {
|
|
53
|
+
if (v.length > maxString && !keep.has(key)) {
|
|
54
|
+
note(`${path} (characters)`, maxString, v.length);
|
|
55
|
+
return `${cutString(v, maxString)}…`;
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(v)) {
|
|
60
|
+
const cap = keep.has(key) ? Number.POSITIVE_INFINITY : rule.fields?.[key] ?? maxItems;
|
|
61
|
+
const kept = v.length > cap ? v.slice(0, cap) : v;
|
|
62
|
+
if (v.length > cap)
|
|
63
|
+
note(`${path}[]`, cap, v.length);
|
|
64
|
+
return kept.map((item) => walk(item, `${path}[]`, key, depth + 1));
|
|
65
|
+
}
|
|
66
|
+
if (isRec(v)) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, x] of Object.entries(v)) {
|
|
69
|
+
if (depth === 0 && rule.drop?.includes(k)) {
|
|
70
|
+
note(k, 0, Array.isArray(x) ? x.length : 1);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (keep.has(k)) {
|
|
74
|
+
out[k] = x;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out[k] = walk(x, path ? `${path}.${k}` : k, k, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
return v;
|
|
82
|
+
};
|
|
83
|
+
const out = walk(value, "", "", 0);
|
|
84
|
+
if (isRec(out) && cuts.length > 0) {
|
|
85
|
+
const prior = Array.isArray(out.trimmed) ? out.trimmed : [];
|
|
86
|
+
return { value: { ...out, trimmed: [...prior, ...cuts] }, cuts };
|
|
87
|
+
}
|
|
88
|
+
return { value: out, cuts };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* One tool answer (its JSON text) under a character cap. The caps shrink until the text fits: first the rule's own
|
|
92
|
+
* item caps, then halves of them down to two items, then the largest top-level lists are left out. The answer is
|
|
93
|
+
* never cut mid-JSON; a model always reads a complete document with its `trimmed` notes. Non-JSON text (an error
|
|
94
|
+
* line) is cut at the cap with a marker.
|
|
95
|
+
*/
|
|
96
|
+
export function compactToolText(text, cap, rule = {}) {
|
|
97
|
+
if (text.length <= cap)
|
|
98
|
+
return { text, cuts: [], capped: false, chars: text.length };
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(text);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
const cut = `${cutString(text, Math.max(0, cap - 40))} [cut at ${cap} characters]`;
|
|
105
|
+
return { text: cut, cuts: [], capped: true, chars: cut.length };
|
|
106
|
+
}
|
|
107
|
+
let maxItems = Number.isFinite(rule.maxItems ?? 10) ? (rule.maxItems ?? 10) : 10;
|
|
108
|
+
let fields = rule.fields;
|
|
109
|
+
for (let round = 0; round < 8; round += 1) {
|
|
110
|
+
const { value, cuts } = compactValue(parsed, { ...rule, maxItems, fields });
|
|
111
|
+
const out = JSON.stringify(value);
|
|
112
|
+
if (out.length <= cap)
|
|
113
|
+
return { text: out, cuts, capped: true, chars: out.length };
|
|
114
|
+
if (maxItems <= 2)
|
|
115
|
+
break;
|
|
116
|
+
maxItems = Math.max(2, Math.floor(maxItems / 2));
|
|
117
|
+
fields = fields ? Object.fromEntries(Object.entries(fields).map(([k, n]) => [k, Math.max(2, Math.floor(n / 2))])) : undefined;
|
|
118
|
+
}
|
|
119
|
+
/* Still over: leave out the top-level lists one by one, the one that costs the most at the final caps first. */
|
|
120
|
+
if (isRec(parsed)) {
|
|
121
|
+
const rec = { ...parsed };
|
|
122
|
+
const lists = Object.entries(rec)
|
|
123
|
+
.filter(([k, v]) => Array.isArray(v) && !KEEP_KEYS.has(k) && !(rule.keepKeys ?? []).includes(k))
|
|
124
|
+
.map(([k, v]) => [k, JSON.stringify(v.slice(0, fields?.[k] ?? maxItems)).length])
|
|
125
|
+
.sort((a, b) => b[1] - a[1]);
|
|
126
|
+
const dropped = [];
|
|
127
|
+
for (const [k] of lists) {
|
|
128
|
+
dropped.push(k);
|
|
129
|
+
const { value, cuts } = compactValue(rec, { ...rule, maxItems, fields, drop: [...(rule.drop ?? []), ...dropped] });
|
|
130
|
+
const out = JSON.stringify(value);
|
|
131
|
+
if (out.length <= cap)
|
|
132
|
+
return { text: out, cuts, capped: true, chars: out.length };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const fallback = JSON.stringify({ truncated: true, chars: text.length, limit: cap, note: "Answer too large for the desk even in compact form. Narrow the call: one symbol, fewer days or a smaller limit." });
|
|
136
|
+
return { text: fallback, cuts: [], capped: true, chars: fallback.length };
|
|
137
|
+
}
|
|
138
|
+
/** The compact rules of the thirteen large tools (measured 24 Oct 2026, olcum/mcp_arac_sayimi.md). Each handler cuts
|
|
139
|
+
* its own lists for detail "compact" (it knows which end of a series is the newest and writes one `trimmed` entry
|
|
140
|
+
* with consistent counts); these rules only round figures, shorten free text and leave grids out, so every list
|
|
141
|
+
* stays as the handler cut it (maxItems Infinity). A key here must be a tool that declares `detail`
|
|
142
|
+
* (test/compact.test.mjs checks the two sets agree). */
|
|
143
|
+
const NO_LIST_CUT = Number.POSITIVE_INFINITY;
|
|
144
|
+
export const COMPACT_RULES = {
|
|
145
|
+
get_liqmap: { maxItems: NO_LIST_CUT, drop: ["heatmap_cells", "x_labels", "y_labels", "band_prices", "price_path", "candles"], more: 'pass detail "full" for every level and print' },
|
|
146
|
+
get_turkey_premium: { maxItems: NO_LIST_CUT, maxString: 300 },
|
|
147
|
+
get_correlations: { maxItems: NO_LIST_CUT },
|
|
148
|
+
get_hl_whales: { maxItems: NO_LIST_CUT },
|
|
149
|
+
get_options_flow: { maxItems: NO_LIST_CUT, maxString: 300 },
|
|
150
|
+
get_funding_arbitrage: { maxItems: NO_LIST_CUT },
|
|
151
|
+
get_desk_notes: { maxItems: NO_LIST_CUT, keepKeys: ["text", "headline", "x_text", "history_line"] },
|
|
152
|
+
get_positioning: { maxItems: NO_LIST_CUT },
|
|
153
|
+
get_coverage: { maxItems: NO_LIST_CUT, maxString: 200, keepKeys: ["note_en"], drop: ["matrix"], more: "omit detail for the venue by product matrix" },
|
|
154
|
+
get_new_listings: { maxItems: NO_LIST_CUT },
|
|
155
|
+
get_venue_markets: { maxItems: NO_LIST_CUT },
|
|
156
|
+
get_tokenized_stocks: { maxItems: NO_LIST_CUT, maxString: 300 },
|
|
157
|
+
get_factor_board: { maxItems: NO_LIST_CUT, maxString: 200, keepKeys: ["interpretationEn", "todayText", "bandEn"] },
|
|
158
|
+
};
|
|
159
|
+
export const COMPACT_TOOL_NAMES = Object.keys(COMPACT_RULES);
|
package/dist/tools.d.ts
CHANGED
|
@@ -24,5 +24,8 @@ export type RegisterOptions = {
|
|
|
24
24
|
/** Per-call Authorization value, for hosts that forward their caller's key
|
|
25
25
|
* (the hosted endpoint). Takes precedence over apiKey when it returns one. */
|
|
26
26
|
authorizationProvider?: () => string | undefined;
|
|
27
|
+
/** Extra request headers for every outbound fetch, read per call (1.1.0): the hosted AI desk marks the member depth
|
|
28
|
+
* of the signed-in member it answers for. Never a credential; those go through apiKey or authorizationProvider. */
|
|
29
|
+
headersProvider?: () => Record<string, string> | undefined;
|
|
27
30
|
};
|
|
28
31
|
export declare function registerByKaranteliTools(target: McpServer, options?: RegisterOptions): void;
|
package/dist/tools.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* instance, so every tool has exactly one definition.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
|
+
import { COMPACT_RULES, compactValue } from "./compact.js";
|
|
8
9
|
import { buildProvenance, pageForApiPath } from "./provenance.js";
|
|
9
10
|
export function registerByKaranteliTools(target, options) {
|
|
10
11
|
const withTitle = (def) => def.title && !(def.annotations && "title" in def.annotations) ? { ...def, annotations: { ...(def.annotations ?? {}), title: def.title } } : def;
|
|
@@ -16,6 +17,18 @@ export function registerByKaranteliTools(target, options) {
|
|
|
16
17
|
const USER_AGENT = `bykaranteli-mcp/${options?.version ?? "dev"} (+https://github.com/bykarantelicom/bykaranteli-mcp)`;
|
|
17
18
|
const TIMEOUT_MS = 15_000;
|
|
18
19
|
const STATIC_KEY = (options?.apiKey ?? process.env.BYKARANTELI_API_KEY ?? "").trim();
|
|
20
|
+
/* headersProvider output, lower-cased, without anything that could carry a credential or change the exchange:
|
|
21
|
+
* the fixed headers and the forwarded authorization always win (package review 24 Oct 2026). */
|
|
22
|
+
const BLOCKED_EXTRA = new Set(["authorization", "proxy-authorization", "cookie", "x-api-key", "accept", "user-agent", "host", "content-type", "content-length"]);
|
|
23
|
+
const extraHeaders = () => {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, v] of Object.entries(options?.headersProvider?.() ?? {})) {
|
|
26
|
+
const key = k.toLowerCase();
|
|
27
|
+
if (!BLOCKED_EXTRA.has(key) && typeof v === "string")
|
|
28
|
+
out[key] = v;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
19
32
|
const authorizationFor = () => {
|
|
20
33
|
/* A host that forwards its callers' keys (authorizationProvider) never falls back to a static key: on the
|
|
21
34
|
* hosted endpoint that fallback would have let every keyless caller act as whatever key the process held
|
|
@@ -39,6 +52,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
39
52
|
headers: {
|
|
40
53
|
accept: "application/json",
|
|
41
54
|
"user-agent": USER_AGENT,
|
|
55
|
+
...extraHeaders(),
|
|
42
56
|
...(authorization ? { authorization } : {}),
|
|
43
57
|
},
|
|
44
58
|
});
|
|
@@ -92,7 +106,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
92
106
|
truncated: true,
|
|
93
107
|
chars: text.length,
|
|
94
108
|
limit: MAX_RESULT_CHARS,
|
|
95
|
-
note: "Result too large for one context window. Narrow the call: fewer hours or days, a
|
|
109
|
+
note: "Result too large for one context window. Narrow the call: a single symbol, fewer hours or days, a smaller limit, or detail \"compact\" where the tool offers it, then call again.",
|
|
96
110
|
}),
|
|
97
111
|
}],
|
|
98
112
|
};
|
|
@@ -133,6 +147,17 @@ export function registerByKaranteliTools(target, options) {
|
|
|
133
147
|
function compact(data, path, cut, rowsField) {
|
|
134
148
|
return trimmedAnswer(withProvenance(data, path), cut, rowsField);
|
|
135
149
|
}
|
|
150
|
+
const DETAIL = z
|
|
151
|
+
.union([z.boolean(), z.enum(["compact", "full"])])
|
|
152
|
+
.optional()
|
|
153
|
+
.describe('Optional. "compact": a shorter page than the default (fewer rows, newest points kept, rounded figures, long text cut, grids left out; every cut named in trimmed). "full" or true: the whole route answer, still under the 100,000 character ceiling (the largest boards answer the ceiling note then: narrow them with symbol, limit or days instead). Omitted or false: the default first page.');
|
|
154
|
+
const wantsFull = (d) => d === true || d === "full";
|
|
155
|
+
const wantsCompact = (d) => d === "compact";
|
|
156
|
+
/** ok(page), or ok(the compact page) when the caller asked for detail \"compact\": rounding, text and grid rules of
|
|
157
|
+
* src/compact.ts; the lists were cut by the handler. */
|
|
158
|
+
function okDetail(name, detail, page) {
|
|
159
|
+
return ok(wantsCompact(detail) ? compactValue(page, COMPACT_RULES[name]).value : page);
|
|
160
|
+
}
|
|
136
161
|
/** The newest `days` distinct dates of rows carrying a `date` field, in the route's order. */
|
|
137
162
|
function newestDates(rows, days) {
|
|
138
163
|
const dates = [...new Set(rows.map((r) => (isRec(r) ? String(r.date) : "")))].filter(Boolean).sort().reverse();
|
|
@@ -361,25 +386,29 @@ export function registerByKaranteliTools(target, options) {
|
|
|
361
386
|
});
|
|
362
387
|
server.registerTool("get_funding_arbitrage", {
|
|
363
388
|
title: "Cross-exchange funding arbitrage opportunities",
|
|
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.",
|
|
389
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
365
390
|
inputSchema: {
|
|
366
391
|
symbol: z.string().optional().describe("Optional. One perp, e.g. BTCUSDT or BTC: its row with every venue leg."),
|
|
392
|
+
detail: DETAIL,
|
|
367
393
|
},
|
|
368
394
|
annotations: READ_ONLY,
|
|
369
|
-
}, async ({ symbol }) => {
|
|
395
|
+
}, async ({ symbol, detail }) => {
|
|
370
396
|
try {
|
|
371
397
|
const d = (await fetchJson("/api/public/funding-arb"));
|
|
372
398
|
const full = { ...d, source: `${PUBLIC_URL}/funding-arb`, provenance: provenanceFor(d, "/api/public/funding-arb") };
|
|
373
399
|
const rows = list(d.rows).filter(isRec);
|
|
374
400
|
if (symbol) {
|
|
401
|
+
/* A filtered call keeps every leg of that perp at every detail level. */
|
|
375
402
|
const want = normalizeSymbol(symbol);
|
|
376
403
|
const row = rows.find((r) => r.symbol === want);
|
|
377
404
|
if (!row)
|
|
378
405
|
return ok({ generatedAt: d.generatedAt, note: `${want} is not on the funding arbitrage board. Symbols: ${rows.map((r) => String(r.symbol)).join(", ")}` });
|
|
379
|
-
return
|
|
406
|
+
return okDetail("get_funding_arbitrage", detail, { ...full, rows: [row] });
|
|
380
407
|
}
|
|
381
|
-
|
|
382
|
-
|
|
408
|
+
if (wantsFull(detail))
|
|
409
|
+
return ok(full);
|
|
410
|
+
const LEGS = wantsCompact(detail) ? 3 : 5;
|
|
411
|
+
return okDetail("get_funding_arbitrage", detail, trimmedAnswer(full, (rec, t) => {
|
|
383
412
|
let kept = 0;
|
|
384
413
|
let total = 0;
|
|
385
414
|
rec.rows = rows.map((r) => {
|
|
@@ -530,12 +559,32 @@ export function registerByKaranteliTools(target, options) {
|
|
|
530
559
|
});
|
|
531
560
|
server.registerTool("get_options_flow", {
|
|
532
561
|
title: "Options tape: biggest prints and premium flow (BTC + ETH)",
|
|
533
|
-
description: "Call this when the user asks what big options players are buying, about block trades, or whether call or put premium dominates today. Returns 24h call vs put premium bought (net of the sold legs of the same multi-leg block or combo, so a spread counts its net premium), the block-trade share, the multi-leg structure count, and the largest prints of the last 48 hours with strikes, premium, IV, structure id and venue (Deribit or OKX). Updated every 15 minutes.",
|
|
534
|
-
inputSchema: {},
|
|
562
|
+
description: "Call this when the user asks what big options players are buying, about block trades, or whether call or put premium dominates today. Returns 24h call vs put premium bought (net of the sold legs of the same multi-leg block or combo, so a spread counts its net premium), the block-trade share, the multi-leg structure count, and the largest prints of the last 48 hours with strikes, premium, IV, structure id and venue (Deribit or OKX). Updated every 15 minutes. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
563
|
+
inputSchema: { detail: DETAIL },
|
|
535
564
|
annotations: READ_ONLY,
|
|
536
|
-
}, async () => {
|
|
565
|
+
}, async ({ detail }) => {
|
|
537
566
|
try {
|
|
538
|
-
|
|
567
|
+
const path = "/api/public/options-flow";
|
|
568
|
+
const data = await fetchJson(path);
|
|
569
|
+
if (!wantsCompact(detail))
|
|
570
|
+
return ok(withProvenance(data, path));
|
|
571
|
+
/* compact: the first 8 of each per-asset list (the route orders them largest first), the 48h top prints whole. */
|
|
572
|
+
return okDetail("get_options_flow", detail, compact(data, path, (rec, t) => {
|
|
573
|
+
if (!isRec(rec.assets))
|
|
574
|
+
return;
|
|
575
|
+
const assets = {};
|
|
576
|
+
for (const [asset, value] of Object.entries(rec.assets)) {
|
|
577
|
+
if (!isRec(value)) {
|
|
578
|
+
assets[asset] = value;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const out = { ...value };
|
|
582
|
+
for (const key of ["by_expiry", "by_strike", "by_venue", "structures"])
|
|
583
|
+
out[key] = t.head(value[key], 8, `assets.${asset}.${key}`, "omit detail for the default page");
|
|
584
|
+
assets[asset] = out;
|
|
585
|
+
}
|
|
586
|
+
rec.assets = assets;
|
|
587
|
+
}));
|
|
539
588
|
}
|
|
540
589
|
catch (err) {
|
|
541
590
|
return fail(err);
|
|
@@ -632,7 +681,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
632
681
|
});
|
|
633
682
|
server.registerTool("get_tokenized_stocks", {
|
|
634
683
|
title: "Tokenized stocks onchain: supply, premium to the real share, DEX pools and exchange pairs",
|
|
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.",
|
|
684
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
636
685
|
inputSchema: {
|
|
637
686
|
underlying: z.string().regex(/^[A-Za-z0-9.]{1,16}$/).optional().describe("Stock ticker, e.g. TSLA; omit for the whole board."),
|
|
638
687
|
/* Issuer ids mirror TOKENIZED_ISSUER_IDS in web/src/lib/tokenized-equities/meta.ts (the hosted tool reads that list
|
|
@@ -643,9 +692,10 @@ export function registerByKaranteliTools(target, options) {
|
|
|
643
692
|
.describe("One issuer: xstocks, robinhood, coinbase, bstocks, ondo, backpack, gstocks, reality, superstate, figure, dinari or securitize; omit for every issuer."),
|
|
644
693
|
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)."),
|
|
645
694
|
top: z.number().int().min(1).max(200).optional().describe("Rows by supply (default 10, max 200)."),
|
|
695
|
+
detail: DETAIL,
|
|
646
696
|
},
|
|
647
697
|
annotations: READ_ONLY,
|
|
648
|
-
}, async ({ underlying, issuer, chain, top }) => {
|
|
698
|
+
}, async ({ underlying, issuer, chain, top, detail }) => {
|
|
649
699
|
try {
|
|
650
700
|
const params = new URLSearchParams();
|
|
651
701
|
if (underlying)
|
|
@@ -655,13 +705,13 @@ export function registerByKaranteliTools(target, options) {
|
|
|
655
705
|
if (chain)
|
|
656
706
|
params.set("chain", chain);
|
|
657
707
|
/* 0.35.0: 10 rows by default (20 full rows were 113k characters). */
|
|
658
|
-
params.set("top", String(top ?? 10));
|
|
708
|
+
params.set("top", String(top ?? (wantsFull(detail) ? 200 : wantsCompact(detail) ? 6 : 10)));
|
|
659
709
|
const path = `/api/public/tokenized-stocks?${params.toString()}`;
|
|
660
710
|
const data = await fetchJson(path);
|
|
661
|
-
if (underlying)
|
|
662
|
-
return
|
|
663
|
-
const N = 3;
|
|
664
|
-
return
|
|
711
|
+
if (underlying || wantsFull(detail))
|
|
712
|
+
return okDetail("get_tokenized_stocks", detail, withProvenance(data, path));
|
|
713
|
+
const N = wantsCompact(detail) ? 2 : 3;
|
|
714
|
+
return okDetail("get_tokenized_stocks", detail, compact(data, path, (rec, t) => {
|
|
665
715
|
const kept = { wrappers: 0, cex: 0 };
|
|
666
716
|
const total = { wrappers: 0, cex: 0 };
|
|
667
717
|
rec.rows = list(rec.rows).map((r) => {
|
|
@@ -771,16 +821,17 @@ export function registerByKaranteliTools(target, options) {
|
|
|
771
821
|
});
|
|
772
822
|
server.registerTool("get_factor_board", {
|
|
773
823
|
title: "Factor board: what followed days like today across recorded metrics",
|
|
774
|
-
description: "Call this when the user asks which indicators currently sit in an unusual band, whether a metric's current level historically preceded BTC moves, or for a cross-metric conditional overview. Returns every recorded metric in its historical band with the median 7-day BTC move that followed versus the base rate, with an n >= 30 gate; distributions, not forecasts. Without limit the first 40 rows in the board's own order (rows_total counts every row, trimmed names the cut); pass limit up to 400 for the whole board.",
|
|
824
|
+
description: "Call this when the user asks which indicators currently sit in an unusual band, whether a metric's current level historically preceded BTC moves, or for a cross-metric conditional overview. Returns every recorded metric in its historical band with the median 7-day BTC move that followed versus the base rate, with an n >= 30 gate; distributions, not forecasts. Without limit the first 40 rows in the board's own order (rows_total counts every row, trimmed names the cut); pass limit up to 400 for the whole board. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
775
825
|
inputSchema: {
|
|
776
826
|
limit: z.coerce.number().int().min(1).max(400).optional().describe("Optional. Rows to return, 1..400 (default 40)"),
|
|
827
|
+
detail: DETAIL,
|
|
777
828
|
},
|
|
778
829
|
annotations: READ_ONLY,
|
|
779
|
-
}, async ({ limit }) => {
|
|
830
|
+
}, async ({ limit, detail }) => {
|
|
780
831
|
try {
|
|
781
832
|
/* 0.36.1: 79 rows of about 600 characters answered 46,000 characters without arguments; a first page of 40. */
|
|
782
|
-
const n = limit ?? 40;
|
|
783
|
-
return
|
|
833
|
+
const n = wantsFull(detail) ? 400 : limit ?? (wantsCompact(detail) ? 15 : 40);
|
|
834
|
+
return okDetail("get_factor_board", detail, compact(await fetchJson("/api/public/factors"), "/api/public/factors", (rec, t) => {
|
|
784
835
|
rec.rows = t.head(rec.rows, n, "rows", "pass limit up to 400");
|
|
785
836
|
}, "rows"));
|
|
786
837
|
}
|
|
@@ -969,11 +1020,12 @@ export function registerByKaranteliTools(target, options) {
|
|
|
969
1020
|
});
|
|
970
1021
|
server.registerTool("get_venue_markets", {
|
|
971
1022
|
title: "Exchange coverage: OI, volume, funding and pegs across every exchange we snapshot",
|
|
972
|
-
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.",
|
|
1023
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
973
1024
|
inputSchema: {
|
|
974
1025
|
symbol: z.string().optional().describe("string, optional base asset, e.g. BTC"),
|
|
975
1026
|
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"),
|
|
976
1027
|
limit: z.coerce.number().int().min(1).max(250).optional().describe("Optional. Venue market rows without symbol, largest open interest first (default 30)."),
|
|
1028
|
+
detail: DETAIL,
|
|
977
1029
|
},
|
|
978
1030
|
annotations: READ_ONLY,
|
|
979
1031
|
}, async (args) => {
|
|
@@ -982,15 +1034,34 @@ export function registerByKaranteliTools(target, options) {
|
|
|
982
1034
|
? `/api/public/venues/oi-history?days=${Math.min(90, Math.floor(args.history_days))}`
|
|
983
1035
|
: args.symbol ? `/api/public/venues/markets?symbol=${encodeURIComponent(args.symbol.toUpperCase())}` : "/api/public/venues/markets";
|
|
984
1036
|
const data = await fetchJson(path);
|
|
985
|
-
|
|
1037
|
+
const c = wantsCompact(args.detail);
|
|
1038
|
+
if (wantsFull(args.detail))
|
|
1039
|
+
return ok(withProvenance(data, path));
|
|
1040
|
+
if (args.history_days) {
|
|
1041
|
+
if (!c)
|
|
1042
|
+
return ok(withProvenance(data, path));
|
|
1043
|
+
/* compact: the hourly history runs oldest first, so the newest 24 points are its tail. */
|
|
1044
|
+
return okDetail("get_venue_markets", args.detail, compact(data, path, (rec, t) => {
|
|
1045
|
+
const points = list(rec.points);
|
|
1046
|
+
if (points.length > 24) {
|
|
1047
|
+
rec.points = points.slice(-24);
|
|
1048
|
+
t.note("points", 24, points.length, "omit detail for every point of the window");
|
|
1049
|
+
}
|
|
1050
|
+
}));
|
|
1051
|
+
}
|
|
1052
|
+
if (args.symbol && !c)
|
|
986
1053
|
return ok(withProvenance(data, path));
|
|
987
1054
|
/* The route lists rows by venue; the board's own ranking is open interest, then 24h volume for the rows
|
|
988
1055
|
* without open interest (spot). */
|
|
989
|
-
const n = Number(args.limit ?? 30);
|
|
1056
|
+
const n = Number(args.limit ?? (c ? 12 : 30));
|
|
990
1057
|
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : -1);
|
|
991
|
-
return
|
|
1058
|
+
return okDetail("get_venue_markets", args.detail, compact(data, path, (rec, t) => {
|
|
992
1059
|
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));
|
|
993
|
-
rec.rows = t.head(ranked, n, "rows", "pass symbol for one coin's rows on every venue, or limit up to 250");
|
|
1060
|
+
rec.rows = t.head(ranked, n, "rows", args.symbol ? "omit detail for every venue of this coin" : "pass symbol for one coin's rows on every venue, or limit up to 250");
|
|
1061
|
+
if (c && !args.symbol) {
|
|
1062
|
+
rec.symbols = t.head(rec.symbols, 12, "symbols", "omit detail for every symbol");
|
|
1063
|
+
rec.coverage = t.head(rec.coverage, 15, "coverage", "omit detail for every feed");
|
|
1064
|
+
}
|
|
994
1065
|
}, "rows"));
|
|
995
1066
|
}
|
|
996
1067
|
catch (err) {
|
|
@@ -1055,12 +1126,31 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1055
1126
|
});
|
|
1056
1127
|
server.registerTool("get_correlations", {
|
|
1057
1128
|
title: "Crypto correlation matrix",
|
|
1058
|
-
description: "Call this when the user asks how correlated two coins are, for decorrelated pairs, or how tightly alts track BTC. Returns the 30-day rolling Pearson correlation matrix of daily returns across the top perpetuals.",
|
|
1059
|
-
inputSchema: {},
|
|
1129
|
+
description: "Call this when the user asks how correlated two coins are, for decorrelated pairs, or how tightly alts track BTC. Returns the 30-day rolling Pearson correlation matrix of daily returns across the top perpetuals. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1130
|
+
inputSchema: { detail: DETAIL },
|
|
1060
1131
|
annotations: READ_ONLY,
|
|
1061
|
-
}, async () => {
|
|
1132
|
+
}, async ({ detail }) => {
|
|
1062
1133
|
try {
|
|
1063
|
-
|
|
1134
|
+
const path = "/api/public/correlations";
|
|
1135
|
+
const data = withProvenance(await fetchJson(path), path);
|
|
1136
|
+
if (!wantsCompact(detail) || !isRec(data))
|
|
1137
|
+
return ok(data);
|
|
1138
|
+
/* compact: the pairs that involve BTC or ETH, each pair once (the whole matrix is 400 cells). */
|
|
1139
|
+
const cells = list(data.cells).filter(isRec);
|
|
1140
|
+
const majors = new Set(["BTCUSDT", "ETHUSDT"]);
|
|
1141
|
+
const seen = new Set();
|
|
1142
|
+
const kept = cells.filter((c) => {
|
|
1143
|
+
const a = String(c.symbolA);
|
|
1144
|
+
const b = String(c.symbolB);
|
|
1145
|
+
if (a === b || (!majors.has(a) && !majors.has(b)))
|
|
1146
|
+
return false;
|
|
1147
|
+
const key = [a, b].sort().join("|");
|
|
1148
|
+
if (seen.has(key))
|
|
1149
|
+
return false;
|
|
1150
|
+
seen.add(key);
|
|
1151
|
+
return true;
|
|
1152
|
+
});
|
|
1153
|
+
return okDetail("get_correlations", detail, { ...data, cells: kept, trimmed: [{ field: "cells", returned: kept.length, total: cells.length, more: "omit detail for the whole matrix" }] });
|
|
1064
1154
|
}
|
|
1065
1155
|
catch (err) {
|
|
1066
1156
|
return fail(err);
|
|
@@ -1068,13 +1158,23 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1068
1158
|
});
|
|
1069
1159
|
server.registerTool("get_new_listings", {
|
|
1070
1160
|
title: "New and delisted perpetual contracts",
|
|
1071
|
-
description: "Call this when the user asks what new perpetuals were listed, which exchange listed a coin first, or about delistings. Returns listings and delistings across every exchange the hourly scan covers.",
|
|
1072
|
-
inputSchema: {
|
|
1161
|
+
description: "Call this when the user asks what new perpetuals were listed, which exchange listed a coin first, or about delistings. Returns listings and delistings across every exchange the hourly scan covers. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1162
|
+
inputSchema: {
|
|
1163
|
+
days: z.number().int().min(1).max(30).optional().describe("Window in days, 1-30 (default 30). Longer listing history is the listings dataset at bykaranteli.com/data."),
|
|
1164
|
+
detail: DETAIL,
|
|
1165
|
+
},
|
|
1073
1166
|
annotations: READ_ONLY,
|
|
1074
1167
|
}, async (args) => {
|
|
1075
1168
|
try {
|
|
1076
1169
|
const path = `/api/public/listings?days=${args.days && args.days > 0 ? Math.floor(args.days) : 30}`;
|
|
1077
|
-
|
|
1170
|
+
const data = await fetchJson(path);
|
|
1171
|
+
if (!wantsCompact(args.detail))
|
|
1172
|
+
return ok(withProvenance(data, path));
|
|
1173
|
+
/* compact: the newest 15 listings and 10 delistings (the route lists newest first). */
|
|
1174
|
+
return okDetail("get_new_listings", args.detail, compact(data, path, (rec, t) => {
|
|
1175
|
+
rec.listed = t.head(rec.listed, 15, "listed", "omit detail for the whole window");
|
|
1176
|
+
rec.delisted = t.head(rec.delisted, 10, "delisted", "omit detail for the whole window");
|
|
1177
|
+
}));
|
|
1078
1178
|
}
|
|
1079
1179
|
catch (err) {
|
|
1080
1180
|
return fail(err);
|
|
@@ -1162,7 +1262,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1162
1262
|
});
|
|
1163
1263
|
server.registerTool("get_liqmap", {
|
|
1164
1264
|
title: "LiqMap: estimated liquidation clusters with real prints overlaid",
|
|
1165
|
-
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
|
|
1265
|
+
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 largest real prints; pass detail \"full\" for all of them. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1166
1266
|
inputSchema: {
|
|
1167
1267
|
symbol: z
|
|
1168
1268
|
.string()
|
|
@@ -1175,7 +1275,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1175
1275
|
.enum(["1h", "4h", "12h", "24h", "3d", "1w", "30d"])
|
|
1176
1276
|
.optional()
|
|
1177
1277
|
.describe("Model window. Default 24h, the only one on the Free plan; Builder and higher plans get every window."),
|
|
1178
|
-
detail:
|
|
1278
|
+
detail: DETAIL,
|
|
1179
1279
|
},
|
|
1180
1280
|
annotations: READ_ONLY,
|
|
1181
1281
|
}, async ({ symbol, timeframe, detail }) => {
|
|
@@ -1186,21 +1286,22 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1186
1286
|
const data = await fetchJson(path);
|
|
1187
1287
|
const page = `/liqmap/${sym.replace(/USDT$/, "").toLowerCase()}`;
|
|
1188
1288
|
const full = { ...data, source: `${PUBLIC_URL}${page}`, provenance: provenanceFor(data, path, page) };
|
|
1189
|
-
if (detail)
|
|
1289
|
+
if (wantsFull(detail))
|
|
1190
1290
|
return ok(full);
|
|
1191
|
-
const N = 30;
|
|
1192
|
-
|
|
1291
|
+
const N = wantsCompact(detail) ? 12 : 30;
|
|
1292
|
+
const P = wantsCompact(detail) ? 10 : 30;
|
|
1293
|
+
return okDetail("get_liqmap", detail, trimmedAnswer(full, (rec, t) => {
|
|
1193
1294
|
if (isRec(rec.real_levels) && Array.isArray(rec.real_levels.levels) && rec.real_levels.levels.length > N) {
|
|
1194
1295
|
/* The levels nearest the price, kept in the route's price order. */
|
|
1195
1296
|
const levels = rec.real_levels.levels;
|
|
1196
1297
|
const dist = (l) => (isRec(l) && typeof l.distance_pct === "number" ? Math.abs(l.distance_pct) : Infinity);
|
|
1197
1298
|
const nearest = new Set([...levels].sort((a, b) => dist(a) - dist(b)).slice(0, N));
|
|
1198
1299
|
rec.real_levels = { ...rec.real_levels, levels: levels.filter((l) => nearest.has(l)) };
|
|
1199
|
-
t.note("real_levels.levels", N, levels.length,
|
|
1300
|
+
t.note("real_levels.levels", N, levels.length, 'pass detail \"full\" for every level');
|
|
1200
1301
|
}
|
|
1201
1302
|
if (isRec(rec.real_liquidations)) {
|
|
1202
|
-
/*
|
|
1203
|
-
rec.real_liquidations = { ...rec.real_liquidations, points: t.head(rec.real_liquidations.points,
|
|
1303
|
+
/* Largest print first (the route orders prints by size). */
|
|
1304
|
+
rec.real_liquidations = { ...rec.real_liquidations, points: t.head(rec.real_liquidations.points, P, "real_liquidations.points", 'pass detail \"full\" for every print') };
|
|
1204
1305
|
}
|
|
1205
1306
|
}));
|
|
1206
1307
|
}
|
|
@@ -1307,14 +1408,15 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1307
1408
|
});
|
|
1308
1409
|
server.registerTool("get_hl_whales", {
|
|
1309
1410
|
title: "Hyperliquid whale tracker: top-300 accounts, long vs short, positions, changes",
|
|
1310
|
-
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.",
|
|
1411
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1311
1412
|
inputSchema: {
|
|
1312
1413
|
coin: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{1,24}$/).optional().describe("One coin, e.g. BTC"),
|
|
1313
1414
|
events: z.boolean().optional().describe("Include the last 200 position change events"),
|
|
1314
1415
|
limit: z.coerce.number().int().min(1).max(200).optional().describe("Optional. Coins and positions without coin, largest first (default 30)."),
|
|
1416
|
+
detail: DETAIL,
|
|
1315
1417
|
},
|
|
1316
1418
|
annotations: READ_ONLY,
|
|
1317
|
-
}, async ({ coin, events, limit }) => {
|
|
1419
|
+
}, async ({ coin, events, limit, detail }) => {
|
|
1318
1420
|
try {
|
|
1319
1421
|
const params = new URLSearchParams();
|
|
1320
1422
|
if (coin)
|
|
@@ -1324,11 +1426,12 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1324
1426
|
const qs = params.toString();
|
|
1325
1427
|
const path = `/api/public/hyperliquid-whales${qs ? `?${qs}` : ""}`;
|
|
1326
1428
|
const data = await fetchJson(path);
|
|
1327
|
-
if (coin)
|
|
1328
|
-
return
|
|
1329
|
-
return
|
|
1330
|
-
|
|
1331
|
-
rec.
|
|
1429
|
+
if (coin || wantsFull(detail))
|
|
1430
|
+
return okDetail("get_hl_whales", detail, withProvenance(data, path));
|
|
1431
|
+
return okDetail("get_hl_whales", detail, compact(data, path, (rec, t) => {
|
|
1432
|
+
const n = limit ?? (wantsCompact(detail) ? 10 : 30);
|
|
1433
|
+
rec.by_coin = t.head(rec.by_coin, n, "by_coin", "pass coin, or limit up to 200");
|
|
1434
|
+
rec.positions = t.head(rec.positions, n, "positions", "pass coin, or limit up to 200");
|
|
1332
1435
|
}, "positions"));
|
|
1333
1436
|
}
|
|
1334
1437
|
catch (err) {
|
|
@@ -1337,23 +1440,25 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1337
1440
|
});
|
|
1338
1441
|
server.registerTool("get_positioning", {
|
|
1339
1442
|
title: "Positioning: long/short ratios, taker buy/sell and CVD across exchanges",
|
|
1340
|
-
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.",
|
|
1443
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1341
1444
|
inputSchema: {
|
|
1342
1445
|
symbol: z.string().trim().toUpperCase().regex(/^[A-Z0-9]{2,24}$/).optional().describe("One Binance symbol, e.g. BTCUSDT"),
|
|
1343
1446
|
limit: z.coerce.number().int().min(1).max(30).optional().describe("Optional. Perps without symbol, most traded first (default 10, up to 30)."),
|
|
1344
1447
|
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."),
|
|
1448
|
+
detail: DETAIL,
|
|
1345
1449
|
},
|
|
1346
1450
|
annotations: READ_ONLY,
|
|
1347
|
-
}, async ({ symbol, limit, cvd_points }) => {
|
|
1451
|
+
}, async ({ symbol, limit, cvd_points, detail }) => {
|
|
1348
1452
|
try {
|
|
1349
1453
|
const path = `/api/public/positioning${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ""}`;
|
|
1350
1454
|
const data = await fetchJson(path);
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1455
|
+
const c = wantsCompact(detail);
|
|
1456
|
+
const points = cvd_points ?? (symbol ? (c ? 24 : undefined) : c ? 12 : 24);
|
|
1457
|
+
if (wantsFull(detail) || (symbol && points === undefined))
|
|
1353
1458
|
return ok(withProvenance(data, path));
|
|
1354
|
-
return
|
|
1459
|
+
return okDetail("get_positioning", detail, compact(data, path, (rec, t) => {
|
|
1355
1460
|
if (!symbol)
|
|
1356
|
-
rec.rows = t.head(rec.rows, limit ?? 10, "rows", "pass symbol, or limit up to 30");
|
|
1461
|
+
rec.rows = t.head(rec.rows, limit ?? (c ? 6 : 10), "rows", "pass symbol, or limit up to 30");
|
|
1357
1462
|
if (points !== undefined && isRec(rec.cvd)) {
|
|
1358
1463
|
const kept = {};
|
|
1359
1464
|
const total = {};
|
|
@@ -1385,16 +1490,17 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1385
1490
|
});
|
|
1386
1491
|
server.registerTool("get_desk_notes", {
|
|
1387
1492
|
title: "Desk notes: short dated observations the ByKaranteli desk wrote from its own recorded derivatives data",
|
|
1388
|
-
description: "Call this when the user asks what happened in crypto derivatives today or on a date, what the desk observed, what moved in funding, open interest, liquidations, options, ETF flows or exchange premiums, or wants a dated, quotable sentence with its number and time. Returns the desk's notes newest first: each is one observation written by the desk writer (an AI model) from the platform's own recorded data and checked against it before publishing, with its publish time in UTC, the card's data time, the desk's 0 to 10 news value, the board it read from, the coins it names and its permanent URL. Information about the data, never advice. Default the newest 25 notes of the last 24 hours (count says how many came back; limit up to 100 for more); pass day for one UTC day, since for a window up to 7 days back (50 notes unless limit says otherwise), type for one card type (funding-arb, liqmap-btc, event-liq-burst, ...), lang for zh, ru or ko text.",
|
|
1493
|
+
description: "Call this when the user asks what happened in crypto derivatives today or on a date, what the desk observed, what moved in funding, open interest, liquidations, options, ETF flows or exchange premiums, or wants a dated, quotable sentence with its number and time. Returns the desk's notes newest first: each is one observation written by the desk writer (an AI model) from the platform's own recorded data and checked against it before publishing, with its publish time in UTC, the card's data time, the desk's 0 to 10 news value, the board it read from, the coins it names and its permanent URL. Information about the data, never advice. Default the newest 25 notes of the last 24 hours (count says how many came back; limit up to 100 for more); pass day for one UTC day, since for a window up to 7 days back (50 notes unless limit says otherwise), type for one card type (funding-arb, liqmap-btc, event-liq-burst, ...), lang for zh, ru or ko text. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1389
1494
|
inputSchema: {
|
|
1390
1495
|
day: z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("One UTC day, YYYY-MM-DD"),
|
|
1391
1496
|
since: z.string().trim().max(40).optional().describe("ISO time; notes published at or after it, at most 7 days back (ignored with day)"),
|
|
1392
1497
|
type: z.string().trim().toLowerCase().regex(/^[a-z0-9][a-z0-9-]{0,60}$/).optional().describe("One card type, e.g. funding-arb, liqmap-btc, event-liq-burst"),
|
|
1393
1498
|
lang: z.enum(["en", "zh", "ru", "ko"]).optional().describe("Text language (default en); a note without that translation comes back in English"),
|
|
1394
1499
|
limit: z.number().int().min(1).max(100).optional().describe("Notes to return, 1..100 (default 25 for the last 24 hours, 50 with day or since)"),
|
|
1500
|
+
detail: DETAIL,
|
|
1395
1501
|
},
|
|
1396
1502
|
annotations: READ_ONLY,
|
|
1397
|
-
}, async ({ day, since, type, lang, limit }) => {
|
|
1503
|
+
}, async ({ day, since, type, lang, limit, detail }) => {
|
|
1398
1504
|
try {
|
|
1399
1505
|
const q = new URLSearchParams();
|
|
1400
1506
|
if (day)
|
|
@@ -1408,11 +1514,13 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1408
1514
|
/* 0.36.1: a day of forty notes answered 47,000 characters without arguments; the first page is 25. */
|
|
1409
1515
|
if (limit)
|
|
1410
1516
|
q.set("limit", String(limit));
|
|
1411
|
-
else if (
|
|
1517
|
+
else if (wantsCompact(detail))
|
|
1518
|
+
q.set("limit", "10");
|
|
1519
|
+
else if (!day && !since && !wantsFull(detail))
|
|
1412
1520
|
q.set("limit", "25");
|
|
1413
1521
|
const qs = q.toString();
|
|
1414
1522
|
const path = `/api/public/desk${qs ? `?${qs}` : ""}`;
|
|
1415
|
-
return
|
|
1523
|
+
return okDetail("get_desk_notes", detail, withProvenance(await fetchJson(path), path));
|
|
1416
1524
|
}
|
|
1417
1525
|
catch (err) {
|
|
1418
1526
|
return fail(err);
|
|
@@ -1420,14 +1528,15 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1420
1528
|
});
|
|
1421
1529
|
server.registerTool("get_turkey_premium", {
|
|
1422
1530
|
title: "Turkey Premium Index: what lira buyers pay above the world price, with TRY reference prices from Turkish venues",
|
|
1423
|
-
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.",
|
|
1531
|
+
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. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1424
1532
|
inputSchema: {
|
|
1425
1533
|
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"),
|
|
1426
1534
|
history_days: z.number().int().min(1).max(30).optional().describe("Include 15-minute index history for the pair, 1..30 days"),
|
|
1427
1535
|
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)."),
|
|
1536
|
+
detail: DETAIL,
|
|
1428
1537
|
},
|
|
1429
1538
|
annotations: READ_ONLY,
|
|
1430
|
-
}, async ({ pair, history_days, daily_days }) => {
|
|
1539
|
+
}, async ({ pair, history_days, daily_days, detail }) => {
|
|
1431
1540
|
try {
|
|
1432
1541
|
const q = new URLSearchParams();
|
|
1433
1542
|
if (pair)
|
|
@@ -1437,15 +1546,35 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1437
1546
|
const qs = q.toString();
|
|
1438
1547
|
const path = `/api/public/turkey-premium${qs ? `?${qs}` : ""}`;
|
|
1439
1548
|
const data = await fetchJson(path);
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1549
|
+
const c = wantsCompact(detail);
|
|
1550
|
+
const wantDays = daily_days ?? (pair ? (c ? 3 : undefined) : c ? 3 : 7);
|
|
1551
|
+
if (wantsFull(detail))
|
|
1442
1552
|
return ok(withProvenance(data, path));
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1553
|
+
if (wantDays === undefined && !c)
|
|
1554
|
+
return ok(withProvenance(data, path));
|
|
1555
|
+
return okDetail("get_turkey_premium", detail, compact(data, path, (rec, t) => {
|
|
1556
|
+
if (wantDays !== undefined) {
|
|
1557
|
+
const daily = list(rec.daily);
|
|
1558
|
+
const keep = newestDates(daily, wantDays);
|
|
1559
|
+
const kept = daily.filter((r) => isRec(r) && keep.has(String(r.date)));
|
|
1560
|
+
t.note("daily", kept.length, daily.length, "pass pair, or daily_days up to 30");
|
|
1561
|
+
rec.daily = kept;
|
|
1562
|
+
}
|
|
1563
|
+
if (c) {
|
|
1564
|
+
/* The 15-minute history (history.points) runs oldest first: the newest 24 points are its tail. */
|
|
1565
|
+
if (isRec(rec.history) && Array.isArray(rec.history.points) && rec.history.points.length > 24) {
|
|
1566
|
+
const points = rec.history.points;
|
|
1567
|
+
rec.history = { ...rec.history, points: points.slice(-24) };
|
|
1568
|
+
t.note("history.points", 24, points.length, 'pass detail \"full\" for every point');
|
|
1569
|
+
}
|
|
1570
|
+
if (isRec(rec.pairs)) {
|
|
1571
|
+
const pairs = {};
|
|
1572
|
+
for (const [key, value] of Object.entries(rec.pairs)) {
|
|
1573
|
+
pairs[key] = isRec(value) ? { ...value, venues: t.head(value.venues, 5, `pairs.${key}.venues`, 'pass detail \"full\" for every venue') } : value;
|
|
1574
|
+
}
|
|
1575
|
+
rec.pairs = pairs;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1449
1578
|
}));
|
|
1450
1579
|
}
|
|
1451
1580
|
catch (err) {
|
|
@@ -1667,20 +1796,25 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1667
1796
|
});
|
|
1668
1797
|
server.registerTool("get_coverage", {
|
|
1669
1798
|
title: "Coverage registry: which venues and data types we collect, how, and how fresh",
|
|
1670
|
-
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
|
|
1799
|
+
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 \"full\" for the full registry. Pass detail \"compact\" for a shorter page or \"full\" for the whole answer.",
|
|
1671
1800
|
inputSchema: {
|
|
1672
|
-
detail:
|
|
1801
|
+
detail: DETAIL,
|
|
1673
1802
|
},
|
|
1674
1803
|
annotations: READ_ONLY,
|
|
1675
1804
|
}, async ({ detail }) => {
|
|
1676
1805
|
try {
|
|
1677
1806
|
const path = "/api/public/coverage";
|
|
1678
1807
|
const data = await fetchJson(path);
|
|
1679
|
-
if (detail)
|
|
1808
|
+
if (wantsFull(detail))
|
|
1680
1809
|
return ok(withProvenance(data, path));
|
|
1681
|
-
const more =
|
|
1682
|
-
return
|
|
1683
|
-
|
|
1810
|
+
const more = 'pass detail \"full\" for the full registry';
|
|
1811
|
+
return okDetail("get_coverage", detail, compact(data, path, (rec, t) => {
|
|
1812
|
+
const c = wantsCompact(detail);
|
|
1813
|
+
rec.snapshots = t.head(rec.snapshots, c ? 8 : 30, "snapshots", more);
|
|
1814
|
+
if (c) {
|
|
1815
|
+
rec.orderbook_depth = t.head(rec.orderbook_depth, 8, "orderbook_depth", more);
|
|
1816
|
+
rec.funding_arbitrage = t.head(rec.funding_arbitrage, 10, "funding_arbitrage", more);
|
|
1817
|
+
}
|
|
1684
1818
|
const liq = list(rec.liquidations);
|
|
1685
1819
|
let translated = 0;
|
|
1686
1820
|
rec.liquidations = liq.map((r) => {
|
|
@@ -1772,6 +1906,7 @@ export function registerByKaranteliTools(target, options) {
|
|
|
1772
1906
|
headers: {
|
|
1773
1907
|
accept: "application/json",
|
|
1774
1908
|
"user-agent": USER_AGENT,
|
|
1909
|
+
...extraHeaders(),
|
|
1775
1910
|
authorization,
|
|
1776
1911
|
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
1777
1912
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bykaranteli-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for live crypto derivatives data from bykaranteli.com: funding rates, funding arbitrage, open interest pressure, liquidations, options, ETF flows, Fear & Greed and BTC dominance. 68 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": {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
|
-
"prepare": "npm run build"
|
|
20
|
+
"prepare": "npm run build",
|
|
21
|
+
"test": "node --test test/*.test.mjs",
|
|
22
|
+
"pretest": "npm run -s build"
|
|
21
23
|
},
|
|
22
24
|
"engines": {
|
|
23
25
|
"node": ">=20"
|
|
@@ -63,6 +65,10 @@
|
|
|
63
65
|
"./tools": {
|
|
64
66
|
"types": "./dist/tools.d.ts",
|
|
65
67
|
"default": "./dist/tools.js"
|
|
68
|
+
},
|
|
69
|
+
"./compact": {
|
|
70
|
+
"types": "./dist/compact.d.ts",
|
|
71
|
+
"default": "./dist/compact.js"
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
}
|