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).
| 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.
| 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 |
- Regex —
build/transpile.ts,build/transpileWS.ts→ Python and PHP. Brittle; depends on TS formatting. - AST —
ast-transpilernpm package, used bybuild/csharpTranspiler.tsandbuild/goTranspiler.ts→ C# and Go. More forgiving.
Code in ts/src/ must satisfy both.
ts/src/*.ts— REST exchange implementationsts/src/pro/*.ts— WS exchange implementationsts/src/base/Exchange.ts— base class (partly transpiled; see §4)ts/src/base/ws/{Client,Cache,OrderBook,OrderBookSide,Future,WsClient}.ts— pro basets/src/base/{errors,errorHierarchy,Precise,types,functions}.tsts/src/test/Exchange/test.*.ts— REST unified-method teststs/src/test/Exchange/base/test.<structure>.ts— shared validators (orderBook, ticker, trade, …)ts/src/pro/test/Exchange/test.watch*.ts— WS unified-method teststs/src/test/base/**,ts/src/pro/test/base/**— base unit tests- Static fixtures:
ts/src/test/static/{request,response}/<exchange>.json
These are overwritten by the build:
js/**(tsc output)python/ccxt/*.py,python/ccxt/async_support/*.py(per-exchange)python/ccxt/test/tests_sync.pyand any transpiled testphp/*.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/*.goandgo/v4/pro/*.go(every per-exchange Go file)ts/src/abstract/*.ts(emitted from each exchange'sapiblock)dist/**,build/ccxt.wiki,index.d.ctsREADME.mdexchange tables,wiki/Exchange-Markets*.mdpython/{README.md,LICENSE.txt,keys.json,package.json}(copies)package.jsonversion bumps (usenpm 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.
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.
python/ccxt/base/{errors,types,precise,decimal_to_precision}.pypython/ccxt/async_support/base/{throttler.py, ws/*.py}+ top ofexchange.pyphp/{<ErrorName>.php, Precise.php, Throttler.php},php/static_dependencies/**,php/pro/{Client.php, ArrayCache*.php, OrderBook.php, BaseCache.php}, top ofphp/Exchange.phpcs/ccxt/base/Exchange.*.cs(exceptBaseMethods.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 ingo/v4/<exchange>.go)
Default rule: every new method or behaviour change ships with a test. Adding code without a test needs justification in the PR.
| 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 |
- Write/extend the test first (
ts/src/test/...or static JSON fixture). - Edit
ts/src/. - Fast verify:
npm run tsBuild && npm run lint. - Offline tests in JS first (cheapest):
npm run test-base-rest-js,id-tests-js,request-js,response-js. - Once green in JS, transpile and run in every other language (see §6).
- Only after all five langs pass offline, run a live test:
node run-tests <exchange> --js(and--wsif 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.
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 entryPaste 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-*)| 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.
Applies to humans, agents, and CI — anything hitting a real exchange with real credentials.
- 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.- 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/--responseand 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.jsonLive 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.
The runner resolves credentials from three sources, in this order (each overrides the previous):
keys.jsonat 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.keys.local.jsonat repo root — gitignored. Your real credentials live here. Deep-extendskeys.json, so you only need to specify the fields you're overriding.- Environment variables — pattern
<EXCHANGE_ID>_<CREDENTIAL>upper-cased (BINANCE_APIKEY,BINANCE_SECRET,OKX_PASSWORD,KRAKEN_UID,HYPERLIQUID_WALLETADDRESS,HYPERLIQUID_PRIVATEKEY, …). Only loaded when--loadKeysis passed, and only fills credentials still missing after steps 1–2. The credential names come from each exchange'srequiredCredentialsblock (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.api ↔ urls.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.
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.goIterate 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).
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.
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.
A ts/src/ change is not done until it transpiles cleanly to all five languages.
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)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 scopednpm 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)npm run build # incremental: pre-transpile → transpile → CS → docs
npm run force-build # rebuild everything (very slow — reserve for releases)# 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./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-
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/-wsif base changed - At least one live smoke test on the affected exchange
- Diff contains only
ts/src/**(+ optional hand-written base / static JSON)
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.
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.
/**
* @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[]> { ... }@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 fromparams:@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[]}.@ignorefor 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. Seebinance.ts,kraken.ts. Unified methods (fetchTicker,createOrder, …) must NOT be@ignore.
Transpilers convert these to Python ("""...""" Sphinx), PHP (/** */ PHPDoc), C# (/// XML), Go (// package-level).
| 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.
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'], neverobj.key. - Use
safeString/safeNumber/safeInteger/safeDict/safeList/safeBool. Neverobj['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/.filterarrow callbacks in derived classes. Noinoperator 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 viathis.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 theapiblock.
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. NoPromise<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). UseStr/Int/Numfor nullable scalars:async fetchOrder (id: string, symbol: Str = undefined, params = {}): Promise<Order>. - Use
handle*AndParamsextractors: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 fromparams['network'].
- Typed parser helpers.
safeMarketStructure({...})inparseMarket;safeMarket(marketId, market, delimiter, marketType),safeCurrency,safeCurrencyCode;safeOrder2,safeTickerfor unified post-processing. - Declarative error mapping. In
handleErrors, callthrowExactlyMatchedException(this.exceptions['exact'], errorCode, feedback)andthrowBroadlyMatchedException(this.exceptions['broad'], errorMessage, feedback). Don't writeif (errorCode === ...) throw new ...chains — keep mapping indescribe().exceptions. - eslint-disable for control characters still needed when you use them in a literal. Modern
sign()impls useurlencode()/json()and usually don't trigger it.
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.
- 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.
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.
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.
- Locate the source first. A Python/PHP/C#/Go bug almost always needs a fix in
ts/src/<exchange>.ts(orts/src/pro/<exchange>.ts). Check the file banner —PLEASE DO NOT EDIT THIS FILE, IT IS GENERATEDmeans 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 buildcasually — it's slow and rewrites thousands of files. UsetsBuild+lintfor fast feedback;transpile,transpileCS,transpileGOto spot-check. - Don't commit generated diffs in
js/,python/ccxt/<exchange>.py,php/<exchange>.php,cs/ccxt/exchanges/,go/v4/,ts/src/abstract/, ordist/.go/v4/is an exception because some files likeexchange.go/exchange_*.goare not automatically generated so those can be commited - Always write/update JSDoc when adding/changing a public method (§7).
- Pass
--verbosewhen 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 ... --verboseand capture a static-response fixture. - Transpiled file = debugging scratchpad, not fix location. Port back to
ts/src/; nevergit addthe transpiled file.
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.
.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 / 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.
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