MCP server
An agent connects, discovers three tools, reads their descriptions to understand what data exists, and composes its own requests. No URL to build, no REST schema to parse.
The server speaks the MCP protocol over HTTP transport. It relays each call to the API with your key and returns the API’s values unchanged, under the API’s names.
#Connecting
A URL and a header. The server is closed by default: without a valid X-API-KEY, no tool is announced and every request is refused with 401 (X-Deny-Reason: missing_api_key when the header is absent, invalid_api_key when the key is unknown or revoked).
{
"mcpServers": {
"bytnode": {
"type": "http",
"url": "https://api.bytnode.com/mcp",
"headers": { "X-API-KEY": "your_key" }
}
}
}The exact shape of the configuration file depends on the agent, but the principle does not change.
#The three tools
All three are read-only, free of side effects and idempotent: an agent can replay them without risk.
get_system_info
The diagnostic, and the catalogues an agent needs to target a field. It always returns the health of the service, the live list of symbols and a freshness summary; catalogs adds, in the same call, what the targeting parameters of get_market_data expect. It is the call to make first.
| Parameter | Type | Default | Description |
|---|---|---|---|
include_status | boolean | true | At false, skips the freshness summary. |
catalogs | string[] | — | Any of macro_series, macro_intraday_series, symbol_capabilities, options_expiries. Several at once. |
symbol | string | — | Required by symbol_capabilities: which feeds are live and which metrics are available or blocked, before asking. |
asset | string | — | Required by options_expiries, e.g. BTC: the active expiries with their open interest, put/call ratio and max pain. |
category | string | — | Filters the macro_series catalogue. |
get_market_data
One call, many fields, current value or dated history. The fields served by the REST snapshot share a single request; the others (options, ETF, large trades, a targeted macro series) go to their own route, in parallel. Each field is true for its last point, or an integer N for its last N points.
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol | string | — | The pair of per-pair fields, e.g. BTCUSDT. The live list comes from get_system_info. |
asset | string | — | The asset of the options and ETF fields, e.g. BTC. The asset alone, never a pair. |
series | string | — | The series of macro (optional for the current value) and macro_intraday, e.g. DGS10, EURUSD. |
timeframe | string | 1h | Step of the multi-timeframe fields, of the options history and of macro_intraday. 1w: klines only. |
live | boolean | false | Current value only: adds the candle still forming, marked is_closed: false. |
since_ms / until_ms | integer | — | A dated window in Unix milliseconds. Present: every requested field returns its history. |
min_usd | number | — | USD threshold of trades_large (default 100,000, minimum 1,000) and of liquidations_events. |
expiry | string | — | An expiry, YYYY-MM-DD: options_oi_distribution, and options_max_pain in history, where it is required. |
ticker | string | — | One ETF issuer, e.g. IBIT, for the history of etf_fundamentals. |
dataset | string | — | concentration (default), rewards, difficulty or fee_rates, for the history of btc_mining. |
market | string | all | Market of cvd, buy_sell_ratio, trades_avg_size and orderbook: all (spot + perpetual futures), spot or futures. The tool maps it to the route of that market. |
fields | string | — | CSV projection of the columns of each row, dotted notation for nested fields — like ?fields= in the REST API. |
60 data fields | true | int | — | true for the last point, N for the last N points. The description of each field gives what N counts and its ceiling. |
get_market_indicators
The nineteen technical indicators, computed on demand. symbol, timeframe and indicators are mandatory; only limit has a default, at 1.
await c.call_tool("get_market_indicators", {
"symbol": "BTCUSDT",
"timeframe": "1h",
"limit": 60,
"indicators": [
{"id": "rsi_7", "type": "rsi", "period": 7},
{"id": "rsi_21", "type": "rsi", "period": 21},
{"id": "macd", "type": "macd", "fast": 12, "slow": 26, "signal": 9},
{"id": "adx_14", "type": "adx", "period": 14},
],
})The same rules apply as in REST: an id of your choice for each indicator, unique in the call, and a ceiling of limit + warm-up ≤ 1000.
#Current value or history
The mode follows from a single thing: the presence of a dated window. Without since_ms or until_ms, get_market_data answers with the current value; with one of them, every requested field answers with its history over the window — several dated metrics in one call.
| Request | What each field returns |
|---|---|
| field: true | Its last point. |
| field: N | Its last N points. With a timeframe, its last N buckets: basis, oi_change and the other per-minute series are then aggregated by bucket. |
| since_ms / until_ms | Its whole series over the period, never cut silently. Without a timeframe, the finest step that fits your point budget and your plan is chosen and announced; an imposed timeframe that does not fit is refused before the call, with the exact reason (outside your plan, or too many points for one call) and the step that would work. |
| live: true | The fields listed below gain the candle still forming, provisional. Refused with a window, and when no requested field serves it. |
Fields that serve the candle still forming
klinescvdspread_interexchangetrades_klinetrades_kline_spottrades_kline_futures
#Targeting
Each field declares its target: a pair (symbol), an asset (asset), a catalogue series (series) or none, for whole-market data. Anything that does not fit is refused before the first network call, never absorbed, and every problem is listed in the same message.
| Case | Response |
|---|---|
| A field without its target | An error naming the missing parameter, with an example. |
| asset or series that no requested field expects | Error: the parameter is not expected by any requested field. symbol is the exception, and scopes says which fields it really targets. |
| asset filled in with a pair | Error: asset expects the asset alone. There is no automatic derivation. |
| A parameter the chosen route does not accept | Error: for example expiry on the current value of options_max_pain, which only its history accepts. |
| A timeframe that no field uses | Error: the timeframe applies to no requested field (it applies to the multi-timeframe fields: klines, cvd, trades_kline…). |
| A dated window on a field that has none | Error: the field has no dated history; ask for its current value, without a window. |
#The 60 fields
The names are those of the API, the same for the current value and the history, with these differences: the global market is global_market here and global in the REST snapshot (global is a reserved word in Python, and every field is a parameter of the tool); the market is a parameter here (market: "spot") and a path suffix in REST (/v1/cvd/spot); the catalogues are options_expiries and symbol_capabilities (REST: /v1/options/expiry, /v1/capabilities?symbol=…); and a history says the step the tool used in timeframe_applied, since it may choose it. The detail of each field — what it contains, what N counts, its ceiling, its routes — is in its description, which the agent reads in the tool’s schema, and in the data catalogue.
Twenty-five per-pair fields (symbol)
klinesprice_changetrades_spottrades_futurestrades_largetrades_kline_futures_secondsoifunding_nextfunding_ratefunding_cumulativebasisoi_changeliquidations_eventsliquidations_cumulativeliquidations_big_small_ratioheatmap_clustersorderbookcvdspread_interexchangebuy_sell_ratiotrades_avg_sizetrades_klinetrades_kline_spottrades_kline_futurestokenomics
Eleven per-asset fields (asset)
options_summaryoptions_oioptions_oi_changeoptions_oi_distributionoptions_volumeoptions_put_call_ratiooptions_ivoptions_iv_klinesoptions_max_painoptions_greeksetf_fundamentals
The options fields and etf_fundamentals. The active expiries come from the options_expiries catalogue.
Two per-series fields (series)
macromacro_intraday
macro without series returns the last value of every FRED series; with it, that series alone, and its history requires it. macro_intraday refuses 30m. The two do not carry the same instruments: do not substitute one for the other.
Twenty-two whole-market fields (no target)
global_marketfear_greedmacro_net_liquiditymacro_correlationsmacro_momentummacro_riskbtc_networkbtc_mempoolbtc_feesbtc_miningbtc_mvrvbtc_nvtbtc_active_addressesbtc_revenue_per_difficultyeth_supplyeth_gaseth_stakingeth_deflationeth_defieth_gas_momentumeth_squeezeeth_price_btc
Current value only
price_changetrades_spottrades_futurestrades_largetrades_kline_futures_secondsfunding_nextliquidations_eventsmacro_correlationsmacro_momentummacro_riskoptions_summaryoptions_oi_distributionoptions_greeks
These fields have no dated history: they serve their current value (true, or N where the field counts rows — trades, liquidation events). A since_ms or until_ms window on them is refused rather than ignored — an agent would believe it filtered a period while receiving the latest points.
#The response
The data comes back as the API returns it, under one key per field. A field with a timeframe is keyed field@tf. unavailable and out_of_plan_fields are those of the snapshot.
{
"mode": "latest",
"data": {
"klines@1h": [ … ],
"cvd@1h": [ … ],
"options_summary": { … },
"fear_greed": { … }
},
"scopes": {
"symbol:BTCUSDT": ["klines@1h", "cvd@1h"],
"asset:BTC": ["options_summary"],
"market": ["fear_greed"]
},
"timeframe_applied": "1h",
"unavailable": { … }
}scopes: the real scope of each key —symbol:X,asset:X,series:Xormarket. A whole-market index asked for alongside a symbol is labelledmarket, never with the symbol.mode:latestorhistory; in history,windowrepeats the bounds andrangesgives, per series, what is really covered: step, points, first and last date, andtruncated, with how to fetch the rest.timeframe_applied: the step actually applied, as soon as a field used it;live: present when the candle still forming was asked for.errorsandpartial: a field in error does not block the others. It is reported undererrorsand the response carriespartial: true.
#Errors
Every error starts with Error (HTTP NNN): message. When the API answered, it ends with [code: …], the API’s stable error.code; a request the server refuses before calling the API (an unknown field, a missing target, a window it cannot serve) has no code. A failed tool call is flagged as an MCP error (isError), never returned as data.
| Situation | Behaviour |
|---|---|
| Request without a valid key | 401, X-Deny-Reason: missing_api_key (or invalid_api_key), before a single tool is announced. |
| Invalid request | Error (HTTP 422) listing every problem, before any network call. |
| Family outside the plan | Error (HTTP 403): The 'options' data family is not included in your plan. GET /v1/limits lists the families of your key; https://bytnode.com/pricing lists the plans that open it. [code: plan_family] — the data exists, just not in your plan. |
| Snapshot outside the plan (Free) | An explicit Error (HTTP 403) on the snapshot fields, naming the plan that opens them; the other fields are still served. |
| Rate limit or quota | Error (HTTP 429) with the detail of the API: rate per minute, or monthly quota. |
| One field in error | Reported under errors, the others served, partial: true. The call fails only when no field succeeds. |
| Upstream not answering | One retry on 502, 503, 504 or a network failure. A 500 is not replayed: every request counts against your quota. |
| Timeout | Error (HTTP 504) after thirty seconds without a response. |
| Internal server error | Error (HTTP 500) with the type of the error only; the detail stays in the server log. |