A unified trading API with more than 100 crypto exchanges and prediction markets in JavaScript / TypeScript / Python / C# / PHP / Go / Java
# 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#, Go and Java. 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 `Future`s; 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. **Regex** β `build/transpile.ts`, `build/transpileWS.ts` β Python and PHP. Brittle; depends on TS formatting.
2. **AST** β `ast-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.
The per-language base type files are generated from `ts/src/base/types.ts` by `npm run transpile-types`: `python/ccxt/base/types.py`, `cs/ccxt/base/{Exchange.Types.cs,PredictionTypes.cs}`, `go/v4/exchange_types.go` and `java/lib/src/main/java/io/github/ccxt/types/*.java`. Edit `ts/src/base/types.ts`, then regenerate β CI runs `npm run transpile-types-check` and fails if they drifted. PHP has no type file (unified structures are plain arrays) and `js/` is `tsc` output.
---
## 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` and the type files `cs/ccxt/base/{Exchange.Types.cs,PredictionTypes.cs}` are 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,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` and `Exchange.Types.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 |
| Static WS | `ts/src/test/static/ws/<exchange>.json` | yes | ws message handling / watch* parsing changed β replays canned frames into `handleMessage` via a mocked transport and asserts the watch result (`npm run ws-tests-<lang>`, entry needs the exact client `url`) |
| 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:
```bash
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:
```bash
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.
```bash
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`):
```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):
```bash
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`.
### 5.7 Ad-hoc testing β per-language CLI
Working CLI in every language. Reads same `keys.local.json` / env vars as runner.
```bash
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.
```json
{
"<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
```bash
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)
```bash
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
```bash
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)
```bash
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)
```bash
# 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
```bash
./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. `build/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
```ts
/**
* @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.
- **Never name a variable/param after a target-language reserved word** β `from`, `type`, `id`, `in`, `is`, `with`, `class`, `def`, `lambda`, `global`, `pass`, `print`, `var`, `function`, `list`, `dict`, `string`, `object`, `end`, `fn` break Python/PHP/C#/Go/Java. Use `fromAddress`, `typeVar`, etc.
- **Don't return `Promise<void>`** (leaves bare `void` in Python) and **don't write a bare `return;`** in a value-returning method (C# CS0126/CS0161). Use `Promise<any>` + `return undefined;`, and end such methods with `return undefined;`.
- **Integer division**: `x.length / 2` is a float in Python β wrap any quotient fed to `intToBase16`/array indexing in `this.parseToInt(...)`.
- **`.padStart`/`.padEnd` only on a bare identifier** β `expr().padStart(n,'0')` leaks a function call in PHP; assign to a local first.
- 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 `define`d 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 `APPROVE`s 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/prediction/ source TS β prediction-market exchanges (extend PredictionExchange)
ts/src/base/PredictionExchange.ts prediction base (outcome loading/caching β see .claude/rules/prediction-outcomes.md)
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
.claude/rules/ topic-scoped contributor rules (auto-load via `paths:` frontmatter);
prediction-outcomes.md = the outcome loading/caching pattern
```