Skip to content

Latest commit

 

History

History
569 lines (419 loc) · 33.9 KB

File metadata and controls

569 lines (419 loc) · 33.9 KB

CCXT — Repository Guide for Contributors and AI Agents

CCXT is a unified cryptocurrency trading library with one source of truth (TypeScript) transpiled to JavaScript, Python, PHP, C#, and Go. The most common contributor mistake — especially by AI agents — is editing a generated file or shipping code without tests in all languages.

Authoritative rules: CONTRIBUTING.md (transpiler conventions), wiki/Manual.md (unified API spec), wiki/Requirements.md (new-exchange checklist).


1. Architecture

REST vs Pro (WebSocket)

REST Pro (WebSocket)
Source dir ts/src/<exchange>.ts ts/src/pro/<exchange>.ts
Class class <exchange> extends Exchange class <exchange> extends <exchange>Rest
Method prefix fetch* (one-shot HTTP) watch* / unWatch* (subscribe)
Base ts/src/base/Exchange.ts adds ts/src/base/ws/{Client,Cache,OrderBook,Future,WsClient}.ts
Transport (JS) node-fetch / browser fetch ws npm package

The pro class imports the REST class as <exchange>Rest and extends it. describe() deep-extends the REST describe() to add has.watch* flags and urls.api.ws. WS subscriptions go through client(url) (one Client per URL) and resolve via Futures; live updates land in the right Cache (ArrayCacheBySymbolById, ArrayCacheByTimestamp, etc.). Read ts/src/base/ws/Client.ts and ts/src/pro/binance.ts before editing pro code.

Per-language model

Language Sync Async WS transport Notes
TS / JS n/a native async/await ws / browser WebSocket one ESM module
Python ccxt.<ex> ccxt.async_support.<ex> aiohttp + asyncio sync auto-generated from async
PHP ccxt\<ex> ccxt\async\<ex> ReactPHP promises async auto-generated from sync
C# n/a native Task/async System.Net.WebSockets PascalCase wrappers in cs/ccxt/wrappers/
Go (value, error) returns none gorilla/websocket three files per exchange: <ex>.go, <ex>_api.go, <ex>_wrapper.go

Two transpilers

  1. Regexbuild/transpile.ts, build/transpileWS.ts → Python and PHP. Brittle; depends on TS formatting.
  2. ASTast-transpiler npm package, used by build/csharpTranspiler.ts and build/goTranspiler.ts → C# and Go. More forgiving.

Code in ts/src/ must satisfy both.


2. Source of truth — edit ONLY these

  • ts/src/*.ts — REST exchange implementations
  • ts/src/pro/*.ts — WS exchange implementations
  • ts/src/base/Exchange.ts — base class (partly transpiled; see §4)
  • ts/src/base/ws/{Client,Cache,OrderBook,OrderBookSide,Future,WsClient}.ts — pro base
  • ts/src/base/{errors,errorHierarchy,Precise,types,functions}.ts
  • ts/src/test/Exchange/test.*.ts — REST unified-method tests
  • ts/src/test/Exchange/base/test.<structure>.ts — shared validators (orderBook, ticker, trade, …)
  • ts/src/pro/test/Exchange/test.watch*.ts — WS unified-method tests
  • ts/src/test/base/**, ts/src/pro/test/base/** — base unit tests
  • Static fixtures: ts/src/test/static/{request,response}/<exchange>.json

3. NEVER commit edits to generated files

These are overwritten by the build:

  • js/** (tsc output)
  • python/ccxt/*.py, python/ccxt/async_support/*.py (per-exchange)
  • python/ccxt/test/tests_sync.py and any transpiled test
  • php/*.php, php/async/*.php, php/pro/*.php (per-exchange)
  • cs/ccxt/exchanges/**, cs/ccxt/ws/**, cs/ccxt/api/**, cs/ccxt/wrappers/**
  • cs/ccxt/base/Exchange.BaseMethods.cs (generated portion only)
  • go/v4/*.go and go/v4/pro/*.go (every per-exchange Go file)
  • ts/src/abstract/*.ts (emitted from each exchange's api block)
  • dist/**, build/ccxt.wiki, index.d.cts
  • README.md exchange tables, wiki/Exchange-Markets*.md
  • python/{README.md,LICENSE.txt,keys.json,package.json} (copies)
  • package.json version bumps (use npm run vss)

Generated files start with // PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN. If you see it, find the TS source.


4. Files that are PARTLY hand-written, PARTLY transpiled

The base Exchange class in each language has a delimiter. Everything below is regenerated from ts/src/base/Exchange.ts; everything above is hand-written (HTTP client, polyfills, crypto).

Marker: METHODS BELOW THIS LINE ARE TRANSPILED FROM TYPESCRIPT

File Marker (~line) Above the marker
python/ccxt/base/exchange.py 2547 sync HTTP client, polyfills
python/ccxt/async_support/base/exchange.py 680 aiohttp client, asyncio glue
php/Exchange.php 2771 curl HTTP client, PHP polyfills
php/async/Exchange.php 411 ReactPHP HTTP client

C#: only cs/ccxt/base/Exchange.BaseMethods.cs is generated. Other cs/ccxt/base/Exchange.*.cs are hand-written. Behavioural changes go in ts/src/base/Exchange.ts, not per-language base files.

Hand-written base files (safe to edit)

  • python/ccxt/base/{errors,types,precise,decimal_to_precision}.py
  • python/ccxt/async_support/base/{throttler.py, ws/*.py} + top of exchange.py
  • php/{<ErrorName>.php, Precise.php, Throttler.php}, php/static_dependencies/**, php/pro/{Client.php, ArrayCache*.php, OrderBook.php, BaseCache.php}, top of php/Exchange.php
  • cs/ccxt/base/Exchange.*.cs (except BaseMethods.cs), cs/ccxt/static/**, cs/ccxt/ws/{Client,ArrayCache,Future,OrderBook}.cs
  • Go base lives outside go/v4/ (in separate base packages — confirm via imports in go/v4/<exchange>.go)

5. Testing — TDD-first workflow

Default rule: every new method or behaviour change ships with a test. Adding code without a test needs justification in the PR.

5.1 Test layers

Layer Source Offline? When
Base unit (REST) ts/src/test/base/test.*.ts yes base utility changed
Base unit (WS) ts/src/pro/test/base/test.{cache,orderBook,close}.ts yes ts/src/base/ws/* changed
Unified-method ts/src/test/Exchange/test.<method>.ts (+ pro/test/Exchange/test.watch<method>.ts) yes new unified method / return shape changed
Structure validators ts/src/test/Exchange/base/test.<structure>.ts yes unified return structure changed
Static request ts/src/test/static/request/<exchange>.json yes exchange request URL/body changed
Static response ts/src/test/static/response/<exchange>.json yes exchange response parsing changed
ID tests api block yes new endpoints added
Live real exchange NO smoke-check before merge; never primary gate

5.2 TDD loop

  1. Write/extend the test first (ts/src/test/... or static JSON fixture).
  2. Edit ts/src/.
  3. Fast verify: npm run tsBuild && npm run lint.
  4. Offline tests in JS first (cheapest): npm run test-base-rest-js, id-tests-js, request-js, response-js.
  5. Once green in JS, transpile and run in every other language (see §6).
  6. Only after all five langs pass offline, run a live test: node run-tests <exchange> --js (and --ws if relevant).

A test passing only in TS means nothing — the regex transpiler can silently mangle code that compiles in TS, and AST-transpiled C#/Go can diverge on edge cases.

5.3 Static fixture workflow

Primary regression net for per-exchange behaviour. Regenerated via CLI:

node cli.js <exchange> <method> <args...> --report     # request entry
node cli.js <exchange> <method> <args...> --response   # response entry

Paste output into methods.<methodName> array of the respective JSON, then re-run:

npm run request-tests        # all langs
npm run response-tests       # all langs
# per-lang: npm run request-js/-py/-php/-cs/-go (same for response-*)

5.4 Per-language unified-method test entry points

Lang Entry Lang Entry
TS npm run ti-ts PHP npm run ti-php (sync via --sync)
JS npm run ti-js C# npm run ti-cs
Python npm run ti-py (sync via --sync) Go npm run ti-go

Combined offline matrix: npm run id-tests, request-tests, response-tests, test-base-rest, test-base-ws.

5.5 Live tests (last)

🚨 HARD SAFETY RULES — non-negotiable

Applies to humans, agents, and CI — anything hitting a real exchange with real credentials.

  1. Never risk more than 25 USD equivalent per trade. Compute notional before every createOrder: notional = amount × markPrice. Abort/reduce if ≥ 25 USD. For derivatives, notional is the position value. The cap is per individual trade — including cleanup (a 24 USD buy + 24 USD sell to flatten is fine; 30 USD anything is not). For pairs whose minimum order size already exceeds 25 USD: skip the live test and rely on static fixtures.
  2. Never call exchange.withdraw() against a live exchange. Ever. Not testnet, not sandbox, not "just to verify". Withdraw is fixture-only forever — capture via --report/--response and assert against those.

If you cannot live-test a method under both rules, that's the correct outcome.

node run-tests <exchange>                      # all five langs
node run-tests <exchange> --js                 # one lang
node run-tests <exchange> --ws --python-async  # WS, async python
node run-tests <exchange> --sandbox            # testnet URLs
node run-tests <exchange> --useProxy           # proxy from skip-tests.json

Live tests are the last gate. Public endpoints need no keys; private endpoints do (see §5.6).

Always clean up after a live write-endpoint test in a finally block:

Method Cleanup
createOrder (limit, unfilled) cancelOrder, then fetchOrder to confirm canceled
createOrder (market or filled limit) opposite-side trade of filled amount; derivatives: closePosition or reduce-only
editOrder cancelOrder on resulting id
transfer reverse transfer
setLeverage / setMarginMode / setPositionMode snapshot value first, restore after
withdraw don't live-test; fixtures only

Use the exchange's minimum order size. Log every mutating call. Prefer a sub-account or dedicated low-balance test account — never personal trading keys.

5.6 Testing private endpoints — API keys and sandbox

The runner resolves credentials from three sources, in this order (each overrides the previous):

  1. keys.json at repo root — committed. Holds non-secret defaults (e.g. options.defaultType, sandbox flags, market preferences). Don't put real secrets here — it ships in the repo. Use it as the schema reference for which fields each exchange accepts.
  2. keys.local.json at repo root — gitignored. Your real credentials live here. Deep-extends keys.json, so you only need to specify the fields you're overriding.
  3. Environment variables — pattern <EXCHANGE_ID>_<CREDENTIAL> upper-cased (BINANCE_APIKEY, BINANCE_SECRET, OKX_PASSWORD, KRAKEN_UID, HYPERLIQUID_WALLETADDRESS, HYPERLIQUID_PRIVATEKEY, …). Only loaded when --loadKeys is passed, and only fills credentials still missing after steps 1–2. The credential names come from each exchange's requiredCredentials block (apiKey, secret, password, uid, walletAddress, privateKey, token, twofa).

Shape of keys.local.json (same shape applies to keys.json):

{
    "binance":     { "apiKey": "xxx", "secret": "yyy", "options": { "defaultType": "spot" } },
    "okx":         { "apiKey": "xxx", "secret": "yyy", "password": "passphrase" },
    "hyperliquid": { "walletAddress": "0x...", "privateKey": "0x..." }
}

Fields under each exchange override same-named properties on the constructed instance. options flips per-exchange behaviour (account type, sandbox flags, market preferences).

Env-var usage (CI):

BINANCE_APIKEY=... BINANCE_SECRET=... node run-tests binance --js --loadKeys --private

--loadKeys is required — without it, env vars are ignored even if set.

--sandbox. Swaps urls.apiurls.test (declared in describe()). Throws NotSupported if urls.test is missing.

Where to get keys. Sandbox/testnet is preferred — if urls.test exists in describe(), a testnet exists (Binance, Bybit, OKX demo, Deribit, Coinbase sandbox, BitMEX, Phemex…). Most fund play money on request. Some exchanges (Binance, Bybit, OKX, Bitget, Gate, etc.) also offer a demo trading mode inside the live API — keys generated from the exchange's demo portal hit the live host but trade against simulated balances. CCXT exposes this via the same setSandboxMode(true) / --sandbox flag when the exchange wires it up (some use a header/account-type switch rather than urls.test); check the exchange file before assuming behaviour. When no sandbox or demo exists, use a dedicated low-balance account with withdrawal disabled and an IP allowlist. With no keys you can still cover ~80% via offline tests + live public endpoints.

Private flags. --private (public + private), --privateOnly, --verbose, --debug.

5.7 Ad-hoc testing — per-language CLI

Working CLI in every language. Reads same keys.local.json / env vars as runner.

npm run cli.ts -- binance fetchTicker BTC/USDT --verbose
npm run cli.py -- kraken fetchOHLCV BTC/USDT 1h
npm run cli.cs -- coinbase fetchMarkets
# also: cli.js, cli.php, cli.go

Iterate in the language where the bug shows up (cli.py for a Python-only failure, etc.) — much faster than node run-tests. Always pass --verbose when implementing/debugging an endpoint; prints full HTTP request and raw response (needed for signing/parsing/rate-limit issues). Add --sandbox for testnet. Drives static-fixture capture via --report/--response (§5.3).

5.8 Skipping known-broken tests — skip-tests.json

For transient outages, exchange quirks, or unsupported features — never to silence a regression you introduced.

{
    "<exchange>": {
        "skip": "exchange down",
        "skipWs": "no WS support yet",
        "until": "2026-06-07",
        "preferredSpotSymbol": "ETH/USDT",
        "skipMethods": {
            "fetchOHLCV": "endpoint 500s",
            "ticker": { "spread": "broken bid/ask" }
        },
        "httpProxy": "http://...",
        "wsProxy": "wss://..."
    }
}

Always set until. Skips without expiry rot.

5.9 The has capability flags

Unified-method tests only run when exchange.has['<method>'] === true (in describe()). Adding a new unified method? Set has.<method>: true or the test won't run. Don't lie: true without implementation fails tests; missing/false on something implemented means it's silently untested.

The features block declares finer-grained capabilities (createOrder.triggerPrice, fetchOrders.daysBack), verified by test.features.ts. Keep both accurate.


6. Build & verify — required after every change

A ts/src/ change is not done until it transpiles cleanly to all five languages.

6.1 Fast inner loop

npm run tsBuild     # TS → JS only — fastest sanity check
npm run lint        # ESLint on ts/src/*.ts and ts/src/pro/*.ts
npm run eslint "ts/src/<exchange>.ts"   # lint single exchange (CI scoped)

6.2 Per-language transpile (run all five before declaring done)

npm run transpile         # TS → Python + PHP (regex, REST + WS)
npm run transpileCS       # TS → C# (AST)
npm run transpileCSWs     # C# WebSocket
npm run transpileGO       # TS → Go (AST)
npm run transpileJava     # TS → Java (REST + WS + wrappers)
# scoped (single exchange):
npm run transpileRest --python <ex> && npm run transpileWs --python <ex>
npm run transpileRest -- --php <ex> && npm run transpileWs -- --php <ex>
npm run transpileCsSingle -- <ex>          # REST
npm run transpileCsSingle -- --ws <ex>     # WebSocket
npm run transpileJavaSingle -- <ex>        # REST
npm run transpileJavaSingle -- --ws <ex>   # WebSocket
npm run go-build-single -- <ex1> <ex2> # Go scoped

6.3 Compile each target

npm run buildCS              # dotnet build cs/ccxt.sln
npm run buildGO              # go build -C go ./v4 && go build -C go ./v4/pro
npm run buildJava            # cd java/ && ./gradlew build && cd ../
npm run check-python-syntax  # tox -e qa
npm run check-php-syntax
go -C go build ./tests/main.go   # Go test binary
go fmt .                         # Go format (in go/v4 and go/tests/base)

6.4 Full build (slow)

npm run build           # incremental: pre-transpile → transpile → CS → docs
npm run force-build     # rebuild everything (very slow — reserve for releases)

6.4.1 Offline tests (per-language)

# Base tests (only when important_modified in CI):
npm run test-base-rest-{js,py,php,cs,go}    # REST base tests
npm run test-base-ws-{js,py,php,cs,go}      # WS base tests
npm run test-types-go                        # Go type tests
# ID tests:
npm run id-tests-{js,py,php,cs,go,java}
# Request/response tests (full or scoped with -- <exchange>):
npm run request-{js,py,php,cs,go,java}      # all exchanges
npm run request-py-sync -- <ex> && npm run request-py-async -- <ex>  # Python scoped
npm run request-php-sync -- <ex> && npm run request-php-async -- <ex> # PHP scoped
npm run response-{js,py,php,cs,go,java}     # same pattern

6.4.2 Live tests

./run-tests-simul.sh --js                              # all JS
./run-tests-simul.sh --js "<rest_exchanges>" "<ws_exchanges>"  # scoped
# Same pattern: --python-async, --php-async, --csharp, --go, --java
npm run live-tests -- --csharp && npm run live-tests-ws -- --csharp  # C# full

6.5 Verify-after-change checklist (paste into PR)

  • npm run lint
  • npm run tsBuild
  • npm run transpile (Python + PHP)
  • npm run transpileCS + npm run buildCS
  • npm run transpileGO + npm run buildGO
  • npm run check-python-syntax + npm run check-php-syntax
  • Offline tests touched: request-tests, response-tests, id-tests; test-base-rest/-ws if base changed
  • At least one live smoke test on the affected exchange
  • Diff contains only ts/src/** (+ optional hand-written base / static JSON)

6.6 GitHub Actions CI

Seven parallel workflows (.github/workflows/), each on ubuntu-latest + Node 20. utils/init_actions.sh detects important_modified (base/build/test files → full transpile + all tests) vs scoped (only changed exchanges). On master pushes, generated output is auto-committed.

Lang Workflow Pre-transpile Full transpile Build Live tests
JS js.yml pre-transpile-js ↑ (includes tsc) ./run-tests-simul.sh --js
Python python.yml pre-transpile-py force-transpile-fast-py check-python-syntax ./run-tests-simul.sh --python-async
PHP php.yml pre-transpile-php force-transpile-fast-php check-php-syntax ./run-tests-simul.sh --php-async
C# cs.yml pre-transpile-cs transpileCS && transpileCSWs buildCS ./run-tests-simul.sh --csharp
Go go-app.yml export-exchanges && emitAPI goTranspiler.ts && --ws buildGO + go fmt ./run-tests-simul.sh --go
Java java.yml pre-transpile-java transpileJava buildJava ./run-tests-simul.sh --java
Rust rust.yml early-stage, no transpile/test steps wired up yet

Reproduce locally: npm run export-exchanges && npm run emitAPI first → npm run pre-transpile-<lang> → transpile → build → npm run request-<lang> && npm run response-<lang>./run-tests-simul.sh --<lang> "<ex>" "<ex>" for live.


7. Docstrings — required on every public method

Every public method in ts/src/<exchange>.ts, ts/src/pro/<exchange>.ts, and new methods in ts/src/base/Exchange.ts gets a JSDoc block. They drive npm run build-docs, IDE intellisense (JS/TS), and Python/PHP/C#/Go docstrings — missing/wrong docstrings show everywhere.

Pattern

/**
 * @method
 * @name <id>#<methodName>
 * @description <one-line, lowercase, no trailing period>
 * @see https://docs.<exchange>.com/<endpoint>      // repeat @see per variant (spot/swap/future)
 * @param {string} symbol unified market symbol
 * @param {int} [since] timestamp in ms of the earliest entry to fetch
 * @param {int} [limit] the maximum number of entries to return
 * @param {object} [params] extra exchange-specific parameters
 * @param {string} [params.until] timestamp in ms of the latest entry
 * @returns {object[]} a list of [<structure>](https://docs.ccxt.com/#/?id=<anchor>-structure) objects
 */
async fetchMyTrades (symbol: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}): Promise<Trade[]> { ... }

Rules

  • @name <id>#<methodName><id> matches class id (e.g. binance#fetchTime). Required for docs generator.
  • @description — one line, lowercase, no trailing period.
  • @see — repeat per upstream doc; end-of-line comment when one method spans multiple endpoint variants (// spot, // swap).
  • Types: wrap {type}, use [name] for optional params. Required: @param {string} symbol. Optional: @param {int} [since], @param {string|undefined} [code] for nullables.
  • @param {object} [params] is always present.
  • params.<key> — document every param read from params: @param {float} [params.triggerPrice] ....
  • not used by <id>.<method> — for accepted-but-ignored unified params.
  • @returns — link to manual structure: [order structure](https://docs.ccxt.com/#/?id=order-structure). Arrays as {object[]}.
  • @ignore for internal helpers — public methods that aren't part of the unified API (exchange-specific helpers, signed-amount calculators, request builders) still need JSDoc but must be @ignore'd. See binance.ts, kraken.ts. Unified methods (fetchTicker, createOrder, …) must NOT be @ignore.

Transpilers convert these to Python ("""...""" Sphinx), PHP (/** */ PHPDoc), C# (/// XML), Go (// package-level).

8. When you change behaviour, update the user-facing docs

Changed… Also update
Unified method signature JSDoc + matching section in wiki/Manual.md
Returned structure wiki/Manual.md (<structure>-structure) + validator in ts/src/test/Exchange/base/test.<structure>.ts
New global helper in ts/src/base/Exchange.ts JSDoc; if user-facing, section in wiki/Manual.md
New unified method JSDoc on every implementer, has flag in describe(), test in ts/src/test/Exchange/, section in wiki/Manual.md
Capability / feature flag wiki/Requirements.md if new requirement; features block self-documents
Examples-worthy behaviour Example under examples/ts/ (npm run tsBuildExamples)
End-user usage docs Matching section in .claude/skills/ccxt-{typescript,python,php,csharp,go}/SKILL.md
Top-level summaries (README.md, llms.txt, llms-full.txt) Only for top-level capability additions/removals

User-facing skills under .claude/skills/ccxt-<lang>/ are for callers asking AI assistants "how do I use this?" — separate from this CLAUDE.md (contributor-facing). Public API changes update both: wiki for humans, skills for AI-assisted callers.

9. TS coding rules to keep the transpilers happy

Full list in CONTRIBUTING.md. Top recurring violations:

  • 4-space indent, no tabs. Blank line between methods, no blank lines inside a method body.
  • Single-quoted string keys: obj['key'], never obj.key.
  • Use safeString/safeNumber/safeInteger/safeDict/safeList/safeBool. Never obj['key'] || fallback (breaks in Python/PHP).
  • Avoid safeValue — typeless escape hatch, deprecated when type is known. Use typed variants; fall back only when value is truly any-of-several.
  • Arithmetic via Precise.stringAdd/Sub/Mul/Div/Gt/.... + is string concatenation only.
  • No .includes() — use .indexOf(x) !== -1. No .map/.filter arrow callbacks in derived classes. No in operator on arrays.
  • Always bracket ternaries: (cond) ? a : b. Don't nest them.
  • Control chars: double quotes with inline disable: "\n" // eslint-disable-line quotes.
  • Array length hint: const n = arr.length; on its own line tells regex transpiler it's an array.
  • Send exchange-specific market IDs, never unified symbols: this.market(symbol)['id']. Parse via this.safeSymbol(marketId, market).
  • Crypto/signing must use base methods (this.hmac, this.jwt, this.ecdsa, this.hash, this.totp). No external libs in derived classes.
  • Each defined endpoint becomes an implicit method (publicGetEndpoint). Don't write explicit HTTP wrappers — list URLs in the api block.

Standard patterns (post-TS migration)

Verified across recent (pacifica.ts, weex.ts, hyperliquid.ts, aster.ts) and certified (binance.ts, okx.ts, kraken.ts, bybit.ts):

  • Typed Promise<...> return signatures are mandatory. Promise<Market[]>, Promise<Order>, Promise<OrderBook>, etc. No Promise<any> in new code.
  • Import types from ./base/types.js: Dict, Str, Int, Num, Strings, structure types (Market, Ticker, Trade, Order, OrderBook, Position, Balances, OHLCV). Use Str/Int/Num for nullable scalars: async fetchOrder (id: string, symbol: Str = undefined, params = {}): Promise<Order>.
  • Use handle*AndParams extractors:
    • handleOptionAndParams(params, '<methodName>', '<key>', defaultValue) — option → params → default.
    • handleMarketTypeAndParams('<methodName>', market, params)[type, params], 'spot' | 'swap' | ….
    • handleSubTypeAndParams('<methodName>', market, params)'linear' | 'inverse'.
    • handleNetworkCodeAndParams(params) — unified network code from params['network'].
  • Typed parser helpers. safeMarketStructure({...}) in parseMarket; safeMarket(marketId, market, delimiter, marketType), safeCurrency, safeCurrencyCode; safeOrder2, safeTicker for unified post-processing.
  • Declarative error mapping. In handleErrors, call throwExactlyMatchedException(this.exceptions['exact'], errorCode, feedback) and throwBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback). Don't write if (errorCode === ...) throw new ... chains — keep mapping in describe().exceptions.
  • eslint-disable for control characters still needed when you use them in a literal. Modern sign() impls use urlencode()/json() and usually don't trigger it.

10. Common contributor questions

Q: Testnet exists but --sandbox doesn't work. Add urls.test mirroring urls.api. setSandboxMode(true) swaps them. Some exchanges expose only some endpoints on testnet — set unsupported ones to live URL or guard with NotSupported.

Q: A test is flaky outside my change. Add to skip-tests.json with reason + until: date. Don't delete the test.

Q: How do I scope a build to one exchange? npm run tsBuild (TS→JS only); tsx build/transpile.ts <exchange> (one to Py/PHP); npm run transpileCsSingle, npm run go-build-single. Tests: node run-tests <exchange> --js.

Q: Add a helper method? Reusable across exchanges → ts/src/base/Exchange.ts. Exchange-specific → exchange file. Don't add a base method only one exchange uses.

Q: New unified method vs. extend existing? Match wiki/Manual.md. New unified methods need agreement — check wiki/Requirements.md and discuss first. Exchange-specific tweaks go through params.

Q: requiredCredentials block? Mark apiKey, secret, password, uid, walletAddress, privateKey, token, twofa as true only if the exchange uses them. Runner uses this for env-var names and private-test gating.

Q: Exchange aliases? Files like binanceus.ts, coinbaseadvanced.ts are thin URL/option overrides of a parent. Tests skip aliases (if (exchange.alias) return) — keep behaviour in the parent.

Q: Lint fails on a transpiler-required pattern (e.g. "\n" in double quotes). Use the inline disable: // eslint-disable-line quotes. Mandatory in those spots, not a workaround.

Q: Hard-to-spot bug from a Py/PHP/C#/Go stack trace — can I edit the transpiled file? Yes, as a scratchpad for print/var_dump/Console.WriteLine/fmt.Println. The fix itself must be ported back to ts/src/<exchange>.ts and verified via npm run transpile. Don't git add the transpiled file.

Q: Endpoint response shape doesn't match exchange docs. Trust the live response. Workflow: hit live with npm run cli.ts -- <id> <method> <args> --verbose, capture static-response fixture (--response), link both in the PR. Cite the doc URL in @see but don't assume it's correct.


11. Pull-request etiquette

  • One PR per exchange. Don't bundle multiple exchanges.
  • Commit only ts/src/** (+ rarely hand-written base / static JSON fixtures). Generated files in your diff means you committed build output — undo that.
  • Set the pre-push hook once: git config core.hooksPath .git-templates/hooks.
  • Don't add language-specific behaviour. If something can't be uniform across all five langs, ask in the PR first.

PR title

Conventional commits scoped to the exchange:

<type>(<exchange>): <description>

<type>fix | feat | chore | refactor | docs | test | perf. <exchange> is the lowercase id. Use base for ts/src/base/Exchange.ts, pro for cross-cutting WS plumbing, tests for test-only edits, build for transpiler/build-script changes.

Examples: fix(binance): correct fundingRate sign for short positions, feat(okx): add fetchMyLiquidations.

PR description

Use the §6.5 checklist as the body. Paste actual output (or one-line summary) under each item — checked boxes without evidence aren't review-able. Add ## Summary (1–3 lines: what + why), Fixes #<n> / Refs #<n> if applicable, and ## Notes for sandbox usage / manual repro / edge cases.


12. For AI agents — the rules that catch agents most often

  • Locate the source first. A Python/PHP/C#/Go bug almost always needs a fix in ts/src/<exchange>.ts (or ts/src/pro/<exchange>.ts). Check the file banner — PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED means find the TS.
  • Pattern-match existing exchanges (binance.ts, kraken.ts, okx.ts). Don't invent.
  • TDD: write/update the test with the code (static JSON fixture or unified test).
  • Verify in all five languages (run §6.5). Don't claim done after tsBuild. If you can't run a step, say so — don't skip silently.
  • Don't npm run build casually — it's slow and rewrites thousands of files. Use tsBuild + lint for fast feedback; transpile, transpileCS, transpileGO to spot-check.
  • Don't commit generated diffs in js/, python/ccxt/<exchange>.py, php/<exchange>.php, cs/ccxt/exchanges/, go/v4/, ts/src/abstract/, or dist/. go/v4/ is an exception because some files like exchange.go/exchange_*.go are not automatically generated so those can be commited
  • Always write/update JSDoc when adding/changing a public method (§7).
  • Pass --verbose when implementing/debugging an endpoint.
  • Update docs when behaviour changes (§8: wiki, examples, user-facing skills).
  • Trust live responses over exchange docs. Verify with cli.ts ... --verbose and capture a static-response fixture.
  • Transpiled file = debugging scratchpad, not fix location. Port back to ts/src/; never git add the transpiled file.

13. Keeping this guide alive

Update CLAUDE.md when: a rule is wrong/out-of-date, you hit an uncovered gotcha, a now-standard pattern (≥3 recent files) isn't documented, or you learned something general about the architecture or workflow.

Don't update CLAUDE.md when: the lesson is exchange-specific (comment it in that file), it's already in CONTRIBUTING.md / wiki/Manual.md (link, don't duplicate), or it's a one-off.

Create a skill (.claude/skills/<name>/SKILL.md) when you've done the same multi-step workflow ≥3 times and it would benefit from being scriptable.

Update .claude/skills/ccxt-<lang>/ when public surface area changes (§8).

Keep CLAUDE.md compact. If a section grows large, split into .claude/rules/<topic>.md with paths: frontmatter so it loads only when relevant files are open.

14. Tooling — automated PR review

.claude/agents/ccxt-pr-reviewer.md does an end-to-end review: reads the diff, transpiles and builds in all five languages, runs offline + live smoke tests, probes for race conditions / security / performance / regressions / breaking changes, and posts a single structured review (inline comments + verdict + test checklist + migration notes).

Use the ccxt-pr-reviewer agent to review PR 28543.
Use the ccxt-pr-reviewer agent to review this branch.

Caps inline comments at 12 with severity tags (🚨 Blocker / ⚠️ Concern / 💡 Suggestion / 📝 Nit). Only APPROVEs when zero issues and every test in Phases 3–5 passes — it doesn't approve out of politeness. Read the agent file for the full workflow before relying on its output.

15. Quick repo map

ts/src/                 source TS — REST exchanges, base, REST tests
ts/src/pro/             source TS — WS exchanges, WS tests
ts/src/abstract/        AUTO-GENERATED API method signatures
ts/src/base/Exchange.ts master base (partly transpiled into all langs)
ts/src/base/ws/         WS base (Client, Cache, OrderBook, Future)
ts/src/test/            REST tests (unified methods, base, static fixtures)
ts/src/pro/test/        WS tests
build/                  transpilers + build scripts (transpile.ts = regex)
js/                     GENERATED — tsc output
python/ccxt/            GENERATED + hand-written base/
python/ccxt/async_support/  GENERATED + hand-written base/ + ws/
php/                    GENERATED + hand-written Exchange.php top + errors
php/async/, php/pro/    GENERATED + hand-written ReactPHP plumbing
cs/ccxt/                GENERATED + hand-written base/ (except BaseMethods.cs)
go/v4/                  GENERATED Go (every file is transpiled)
wiki/                   docs (Manual.md = authoritative API spec)
examples/               per-language end-user examples
.claude/skills/         per-language usage skills (/ccxt-python, /ccxt-typescript, …)
                        — public API reference for callers, NOT for editing CCXT