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"}
]
endTwo 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.
has?/2is advertising, not a dispatch guarantee.CCXT.has?(exchange, "fetchX")reflects what upstream CCXT declares the exchange supports in principle — it does not promise thatccxt_clienthas an endpoint mapping wired for that method today. 116 such gaps exist across the 23 priority-tier exchanges (matches CCXT JS semantics: endpoint map is the dispatch gate,hasis advertising). When in doubt, cross-check withCCXT.<Exchange>.__unified_endpoints__/0; for production workloads prefer the raw per-exchange surface.:customsigning — 3 exchanges need a consumer-supplied module.derive,hyperliquid, andlighterclassify as:customsigning and do not ship with a bundled signer. Public endpoints work out of the box; private endpoints raiseArgumentErrorunless you provide acustom_module:implementingCCXT.Signing.Behaviour. Example:{:ok, ex} = CCXT.Exchange.new("hyperliquid", api_key: "…", secret: "…", signing: %{pattern: :custom, custom_module: MyApp.HyperliquidSigner} )A consumer-side fix is unblocked today; upstream-driven convergence is gated on ccxt_extract Phase 10 exotic.
No testnet for 7 exchanges.
Exchange.new(id, sandbox: true)returns{:error, :no_testnet_data}(andExchange.new!raisesArgumentError) foraster,bitfinex,htx,huobi,kraken,kucoin,kucoinfutures— those exchanges don't publish a public testnet. Use production URLs with small-size orders and strict risk limits, or pick a different exchange for dry-run integration.
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 idsPer-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 mappingDocumentation
- ROADMAP.md — active work, priorities, upstream dependencies
- CHANGELOG.md — completed tasks and known limitations
- CLAUDE.md — architecture, design decisions, internal conventions
- CONTRIBUTING.md — how to land fixes, including exchange overrides
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