Skip to main content

BytNode Python SDK

The official Python client of the BytNode crypto market-data API: candles, order flow, derivatives, liquidations, options, macro, on-chain and ETF data — typed, paginated for you, one call away from a pandas DataFrame, plus the real-time WebSocket stream.

pip install https://github.com/bytnode/bytnode-SDK/releases/download/v1.0.0/bytnode-1.0.0-py3-none-any.whl
# with pandas: pip install "bytnode[pandas] @ https://github.com/bytnode/bytnode-SDK/releases/download/v1.0.0/bytnode-1.0.0-py3-none-any.whl"
export BYTNODE_API_KEY=cb_live_...   # create one at https://bytnode.com/dashboard/keys
from bytnode import Bytnode

client = Bytnode()                                   # reads BYTNODE_API_KEY

last = client.klines("BTCUSDT", "1h")                # the latest CLOSED candle
print(last.data.close, last.data.time)               # typed model, datetimes in UTC

candles = client.klines.history("BTCUSDT", "1h", limit=24)   # most recent first
print(candles.data[0].close, len(candles))

from datetime import datetime, timedelta, timezone

since = datetime.now(timezone.utc) - timedelta(hours=12)                      # inside every plan's window
df = client.klines.history.iter("BTCUSDT", "1h", since=since).to_pandas()     # every page, one DataFrame (pip install pandas)

client.close()                                       # or: with Bytnode() as client: ...

Python 3.10+. Synchronous and asyncio clients. Fully typed (py.typed, mypy strict).


The rule: the URL is the method

Every route of the API reference is an attribute named after its path:

Route Method
GET /v1/klines client.klines(symbol, timeframe) — the latest closed candle
GET /v1/klines/history client.klines.history(symbol, timeframe, ...)
GET /v1/funding/rate client.funding.rate(symbol)
GET /v1/funding/rate/history client.funding.rate.history(symbol, ...)
GET /v1/buy-sell-ratio/spot client.buy_sell_ratio.spot(symbol, timeframe)
GET /v1/options/max-pain/history client.options.max_pain.history("BTC", "2026-12-25")
GET /v1/capabilities client.capabilities("BTCUSDT")
GET /v1/global client.global_() (global is a Python keyword: a trailing _)

Dashes become underscores; a Python keyword takes a trailing underscore. Required parameters come first and can be positional; the others are keyword-only. Closed values are typed (timeframe: Timeframe, asset: Asset, order: Order): your editor completes them, and a typo is flagged before the request is sent. Every method carries the docstring of its route.

Responses

A call returns a Response:

resp = client.funding.rate("BTCUSDT")
resp.data            # FundingRate | None  — a model, a list of models, or None
resp.data.rate_pct   # (when resp.data is not None) attributes are the API's own field names
resp.server_time     # when the server answered (rows carry their own `time`)
resp.symbol          # effective symbol of the request (`resp.meta` holds the rest of the envelope)
resp.unavailable     # why `data` is empty: "not_applicable", "no_api_key" or "no_data"
resp.rate_limit      # your rate limit and monthly quota after this call
resp.raw             # the JSON body, untouched

A list response iterates and indexes like a list (for row in resp, resp[0], len(resp)). Models also read like mappings (row["close"], row.get("volume_spot")) and convert back with row.to_dict() or resp.to_dicts(). Dates are datetime objects in UTC.

A field added by the API after this release is kept as an extra attribute: a newer API never breaks an older SDK. If a response ever does not match its model, it is returned unvalidated with a BytnodeWarning instead of raising.

History and pagination

History routes take time bounds as you write them — datetime (naive = UTC), date, an ISO string or epoch milliseconds:

from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
page = client.funding.rate.history("BTCUSDT", since=now - timedelta(days=5), until=now, limit=100)
page.next   # relative URL of the following page, or None (iter() follows it for you)

.iter() follows next for you, page after page, and yields rows lazily:

from datetime import datetime, timedelta, timezone

for row in client.klines.history.iter("ETHUSDT", "4h", since=datetime.now(timezone.utc) - timedelta(days=3)):
    ...

rows = client.oi.history.iter("BTCUSDT", "1h", since=now - timedelta(hours=12), max_items=500).to_list()
  • With since, pages are as large as the route allows (fewest requests, least quota); without it, the route's default page, which stops at your plan's history window instead of exceeding it. page_size= changes it.
  • With since, rows come oldest first (order="asc"); without, most recent first.
  • .pages() yields each page's Response (rate limit, server_time...).
  • An integer that looks like seconds is refused: since=1767225600 raises with the millisecond value to use instead.

How far back you can read depends on your plan (a PermissionDeniedError with code history_depth says so).

pandas

# `to_pandas()` needs pandas: pip install pandas (or install the wheel with the [pandas] extra)
from datetime import datetime, timedelta, timezone

df = client.klines.history.iter("BTCUSDT", "1d", since=datetime.now(timezone.utc) - timedelta(days=29)).to_pandas()  # inside the Free plan's 30-day daily window
df["close"].pct_change().rolling(7).std()  # 7-day volatility

to_pandas() exists on every response and iterator: indexed by time (UTC), ascending, one column per field. flatten=True spreads nested objects into dotted columns. Install the extra: the pandas extra (bytnode[pandas], see Install).

Errors

from bytnode import InvalidRequestError, PermissionDeniedError, RateLimitError

try:
    client.klines("BTCUSD", "1h")
except InvalidRequestError as e:      # 400 / 422
    print(e.code, e.param, e.message) # invalid_symbol symbol "Unknown symbol 'BTCUSD'. Accepted values: ..."
Exception When Useful attributes
InvalidRequestError 400 / 422: a parameter is wrong code, param, message (says what is accepted)
AuthenticationError 401: key missing, invalid or revoked
PermissionDeniedError 403: outside your plan code (plan_family, history_depth, plan_websocket), details
NotFoundError 404: unknown or retired route message names the replacement
RateLimitError 429 retry_after, is_quota
ServerError 5xx (after retries) status
APIConnectionError / APITimeoutError no answer
MissingAPIKeyError no key at all
StreamError / ConnectionLimitError WebSocket code, close_code

All inherit from bytnode.BytnodeError; HTTP ones from bytnode.APIError (status, code, message, param, details, reason, headers, body). Branch on e.code — stable, listed in bytnode.types.ErrorCode — never on the message. Every error carries request_id (quote it to support). A value the SDK checks before sending (an indicator parameter out of range, a time in seconds instead of milliseconds) raises a plain ValueError at once, without a request: it is a bug in the calling code, not an API answer. Closed values such as timeframe are typed (mypy refuses "60m"); sent anyway, they come back from the API as a 422 (invalid_timeframe, with the accepted values). If the API serves another MAJOR contract than the SDK, or a newer MINOR one (additions the SDK does not type yet), a BytnodeWarning says so once.

Retries, timeouts and rate limits

The SDK retries what is worth retrying, with exponential backoff and jitter:

  • 429 rate limit: waits Retry-After (up to 60 s), then retries;
  • 502, 503, 504 and network errors: up to max_retries times (default 3);
  • 500: once;
  • a spent monthly quota is never retried: RateLimitError with is_quota=True at once.
client = Bytnode(timeout=10, max_retries=5)
client.klines.history("BTCUSDT", "1m", request_options={"timeout": 60, "max_retries": 0})

client.last_rate_limit    # RateLimit(limit=600, remaining=598, reset_s=1, quota_limit=..., quota_remaining=...)

Snapshot: many fields in one call

GET /v1/snapshot returns many fields for one symbol in a single request (from the Traders plan). A field without a timeframe is the OBJECT of its route (True or 1); a multi-timeframe field takes a mapping {timeframe: N} and is ALWAYS a list of the N latest closed buckets:

snap = client.snapshot("BTCUSDT", funding_rate=True, basis=True, klines={"1h": 24, "1d": 7},
                       buy_sell_ratio={"5m": 12})
funding = snap.data.funding_rate     # the object of /v1/funding/rate, or None
bars = snap.data.klines_1h           # list[KlineHistoryItem] | None, most recent first
print(bars[0].close if bars else None)
client.snapshot.catalog("BTCUSDT").data.available_fields

Technical indicators

from bytnode import indicators as ind

res = client.indicators("BTCUSDT", "1h", [ind.rsi(14), ind.macd(), ind.bb(20, std=2.5)], limit=200)
res.data.indicators["rsi_14"].values[0]   # IndicatorRow(time=..., value=...): rows newest first
res.data.indicators["macd_12_26_9"].values[0].signal  # named outputs of a multi-output indicator
res.to_pandas()                           # one column per output (macd_12_26_9.signal...), indexed by candle time

Builders are typed and check their bounds locally; ids are generated (rsi_14) or given (ind.rsi(7, id="fast")). Plain dicts work too: {"type": "ema", "period": 50}.

Real-time WebSocket

from bytnode import channels as ch

with client.stream(ch.trades("BTCUSDT"), ch.orderbook("BTCUSDT"), depth=5) as stream:
    for msg in stream:
        if msg.kind == "orderbook":
            print(msg.data.avg_mid_price, msg.data.bids[0])
        else:
            for trade in msg.data:
                print(trade.side, trade.price, trade.quantity)
        if msg.gap:
            print(f"{msg.gap} message(s) lost before this one on {msg.channel}")
  • Channels: ch.trades(symbol, market="futures"|"spot"), ch.orderbook(symbol, market=...), ch.liquidations(symbol) — or the raw names (trades.futures.BTCUSDT, case-insensitive).
  • Reconnects by itself with backoff and subscribes again; stream.reconnects counts it. A refused key, a plan without the stream or the connection ceiling of your plan are raised, not retried.
  • msg.gap counts the messages lost just before msg on an event channel (a jump in seq).
  • stream.subscribe(...) / stream.unsubscribe(...) change the subscription on the fly (all or nothing: one bad channel, none applied).
  • Messages are typed: TradesMessage, LiquidationsMessage, BookMessage.

Async

AsyncBytnode has the same methods, awaited; .iter() becomes async for, the stream async with / async for:

import asyncio
from datetime import datetime, timedelta, timezone

from bytnode import AsyncBytnode, channels as ch

async def main() -> None:
    async with AsyncBytnode() as client:
        btc, eth = await asyncio.gather(client.funding.rate("BTCUSDT"), client.funding.rate("ETHUSDT"))
        since = datetime.now(timezone.utc) - timedelta(hours=12)
        df = await client.klines.history.iter("BTCUSDT", "1h", since=since).to_pandas()
        async with client.stream(ch.trades("BTCUSDT")) as stream:
            async for msg in stream:
                ...

asyncio.run(main())

MCP and command line

The package installs a bytnode command:

bytnode check                         # key valid? latency? rate limit and quota left?
bytnode mcp install cursor            # add the BytNode MCP server to Cursor
bytnode mcp install claude-desktop    # ... or claude-code, vscode, windsurf
bytnode mcp config vscode             # print the snippet to paste by hand (key masked)

mcp install merges the bytnode server into the client's config (a backup is written first) so an AI agent can query BytNode with your key. From Python: bytnode.mcp.install("cursor").

Configuration

Argument Environment Default
api_key BYTNODE_API_KEY —
base_url BYTNODE_BASE_URL https://api.bytnode.com
stream_url BYTNODE_STREAM_URL wss://api.bytnode.com/ws
timeout 30 s per attempt
max_retries 3
default_headers
http_client your own httpx.Client / httpx.AsyncClient (proxies, TLS...)

Use the client as a context manager, or call close(), to release its connections.

MIT licensed (the code of this SDK; the data stays under your BytNode subscription).

Metadata

Release files for bytnode 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bytnode 1.0.2
File Size Uploaded Provenance Actions
bytnode-1.0.2.tar.gz 135.9 kB With provenance Details for bytnode-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bytnode 1.0.2
File Interpreter ABI Platform Actions
bytnode-1.0.2-py3-none-any.whl Python 3 none any Details for bytnode-1.0.2-py3-none-any.whl

Total release size: 275.5 kB

Release files / bytnode-1.0.2.tar.gz

Download URL bytnode-1.0.2.tar.gz
Size 135.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4f5a0adf6c03293f3bfa624a616461c6cda7d9acbe1bf4a06b31bee2cf978641
BLAKE2b-256 checksum
How to use checksums
a26c345ea4e6f0d89e447bff90d7b7ff487874ff9e6924ee347d530cf7144b7d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

Release files / bytnode-1.0.2-py3-none-any.whl

Download URL bytnode-1.0.2-py3-none-any.whl
Size 139.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d8f90c6e557dece14605a6bd1d10f7e9cbd4c1de3706b48f3b4d9789ea04639a
BLAKE2b-256 checksum
How to use checksums
94383f9ed92b9f0e5c331d62dfa7d942143e6e7b1f20f6c48f5e363aad13523b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

Security for bytnode 1.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for bytnode-1.0.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.2 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page