ccxt_client

Elixir client library for cryptocurrency exchanges, generated from CCXT specs via compile-time macros.

Status:0.x — raw endpoint surface is stable; unified translation layer is evolving.

Installation

def deps do
  [
    {:ccxt_client, "~> 0.6"}
  ]
end

Two Contracts

ccxt_client exposes two API surfaces with different stability guarantees.

Raw Endpoints — Stable

Per-exchange generated functions that pass through to the exchange API unchanged. Signing, rate limiting, circuit breakers, and transport are handled; the response body is returned as-is.

{:ok, exchange} = CCXT.Exchange.new("bybit", api_key: key, secret: secret)
{:ok, response} = CCXT.Bybit.public_get_v5_market_tickers(exchange, %{category: "spot"})

Recommended for agents, automated trading, and any consumer that wants to interpret exchange responses directly.

Unified API — Evolving

Standardized cross-exchange methods. Today most methods still return the raw HTTP response map (%{status, body, headers}); normalized structs arrive with Phase 5 parsers (see ROADMAP.md — gated on upstream ccxt_extract Phase 12).

{:ok, %{status: 200, body: body}} = CCXT.fetch_time(exchange)

Zero-arg public methods (fetch_time, fetch_status, fetch_currencies, fetch_markets) are reliable across the priority-tier set. Symbol-bearing methods (fetch_ticker, fetch_order_book, fetch_trades) have per-exchange gaps — some exchanges need market-category params that the unified layer doesn't yet inject. Prefer the raw surface for those until Phase 5 lands.

WebSocket — Early (T92–T94 landed 2026-04-18/19)

{:ok, ws} = CCXT.WS.connect(exchange, :public)
:ok = CCXT.WS.subscribe(ws, ["tickers.BTCUSDT"])
# Messages arrive at the calling process as {:websocket_message, decoded_map}
CCXT.WS.close(ws)

Thin wrapper over zen_websocket driven by per-exchange subscription + auth patterns. 13 exchanges configured; live tidewave smokes through 0.6.0 (post Bundle A / T101 / T102) confirm stream end-to-end on the full configured set — except kucoin, whose WSS host + token come from a REST pre-call that the adapter layer will wire under T97. Callers today pass pre-formatted exchange-native channel strings; spec-driven channel formatting is T97 (upstream-gated). See ROADMAP.md for the WebSocket track status.

Known Caveats

Consumer-facing gotchas not obvious from the API signatures. Full context in CLAUDE.md and ROADMAP.md.

Discovery

CCXT.describe()                      # Library overview
CCXT.describe(CCXT, :fetch_ticker)   # Method signature + params + errors + return shape
CCXT.MCP.tools()                     # MCP tool definitions for agent autodiscovery
CCXT.Registry.exchanges()            # List compiled exchange ids

Per-exchange introspection (generated on every exchange module):

CCXT.Bybit.__spec__()               # Raw spec map (describe output)
CCXT.Bybit.__endpoints__()          # List of %{path, method, authenticated, weight, ...}
CCXT.Bybit.__signing__()            # %{pattern: :hmac_sha256_headers, config: %{...}}
CCXT.Bybit.__tier__()               # "tier1" | "tier2" | "dex"
CCXT.Bybit.__unified_endpoints__()  # Unified-method → endpoint-config mapping

Documentation

Fixing Exchange Bugs Upstream

When an exchange behaves incorrectly (wrong URL, wrong field name, missing auth section, bad error sentinel), fix it in the spec via an upstream override rather than patching ccxt_client. Overrides live in the ccxt_extract repo under priv/overrides/<exchange>.json and apply at any JSON Pointer via OverrideRegistry.apply_all/2. Every overridden path is tagged "override" in the spec's top-level _provenance map (schema 2.0.0+), so consumers can see exactly which fields diverge from raw extraction.

See CONTRIBUTING.md § Exchange Overrides and ccxt_extract/SCHEMA.md § Override Contract for the authoritative format and workflow.

Development

mix deps.get
mix test.json --quiet
mix dialyzer.json --quiet
mix credo --strict --format json