@cexyio/cexy 0.1.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CEXY.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # @cexyio/cexy
2
+
3
+ The official TypeScript/JavaScript SDK for the [CEXY.io](https://cexy.io) REST and WebSocket API.
4
+
5
+ - Typed models generated from the public OpenAPI spec ([cexy-api-spec](https://github.com/cexyio/cexy-api-spec)).
6
+ - Safe by default: retries with backoff, order placement that never duplicates (via `client_order_id`), a client-side rate limiter.
7
+ - A WebSocket client with heartbeat, reconnect and a live order book that applies the sync rules for you.
8
+ - ESM and CommonJS, Node 20+, zero runtime dependencies.
9
+
10
+ > **Status: 0.x.** The API is not yet frozen. It stays 0.x until the exchange ships HMAC request signing,
11
+ > which will change how credentials are sent.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @cexyio/cexy
17
+ # Node only, optional: lets the WebSocket client send a User-Agent (recommended before Node 22)
18
+ npm install ws
19
+ ```
20
+
21
+ ## Quick start: public data
22
+
23
+ ```ts
24
+ import { CexyClient } from "@cexyio/cexy";
25
+
26
+ const cexy = new CexyClient(); // no key needed for market data
27
+
28
+ const markets = await cexy.markets.list();
29
+ const btc = await cexy.markets.get("BTC/USDT");
30
+ const book = await cexy.markets.orderbook("BTC/USDT", { depth: 10 });
31
+ const candles = await cexy.markets.candles("BTC/USDT", { interval: "1h", limit: 24 });
32
+ const { iso } = await cexy.time();
33
+ ```
34
+
35
+ Also: `assets.list()/get()`, `networks.list()`, `fees.get()`, `config()`, `pools.list()/get()`, `markets.trades()`.
36
+
37
+ ## Quick start: your account
38
+
39
+ ```ts
40
+ import { CexyClient } from "@cexyio/cexy";
41
+
42
+ const cexy = new CexyClient({
43
+ apiKey: process.env.CEXY_API_KEY, // ak_your_key_here
44
+ apiSecret: process.env.CEXY_API_SECRET, // your_secret_here
45
+ });
46
+
47
+ const balances = await cexy.account.balances();
48
+ const open = await cexy.trading.openOrders({ symbol: "BTC/USDT" });
49
+
50
+ const placed = await cexy.trading.placeOrder({
51
+ symbol: "BTC/USDT",
52
+ side: "buy",
53
+ type: "limit",
54
+ price: "60000.00", // decimal STRINGS, never numbers
55
+ quantity: "0.0010",
56
+ });
57
+ await cexy.trading.cancelOrder(placed.order.id);
58
+ await cexy.trading.cancelAll({ symbol: "BTC/USDT" }); // { symbol: null } = every market, explicitly
59
+ ```
60
+
61
+ `cancelAll` requires `symbol`: the server treats a missing symbol as "every market", so the SDK makes
62
+ you say so with `{ symbol: null }`. The server allows cancel-all 30 times per minute per account.
63
+
64
+ Give both `apiKey` and `apiSecret`, or neither: passing only one throws at construction.
65
+
66
+ | Namespace | Methods | Scope |
67
+ |---|---|---|
68
+ | `markets` | `list`, `get`, `orderbook`, `trades`, `iterateTrades`, `candles` | public |
69
+ | `assets`, `networks`, `fees`, `pools` | `list`, `get` / `list` / `get` / `list`, `get` | public |
70
+ | `time()`, `config()` | | public |
71
+ | `account` | `balances`, `balance`, `ledger`, `notifications`, `subAccounts`, `apiKeys` (+ iterators) | read |
72
+ | `exports` | `deposits`, `ledger`, `orders`, `trades`, `withdrawals` (CSV text) | read |
73
+ | `wallet` | `deposits`, `deposit`, `withdrawals`, `withdrawal`, `withdrawalAddresses`, `depositAddress` (+ iterators) | read |
74
+ | `trading` | `openOrders`, `order`, `orderByClientId`, `orderHistory`, `trades` (+ iterators) | read |
75
+ | `trading` | `placeOrder`, `cancelOrder`, `cancelAll` | trade |
76
+ | `pools` | `join`, `exit` | trade |
77
+
78
+ `wallet.depositAddress({ asset, network })` **creates** the address on the first call for that
79
+ asset/network (later calls return the same one). Always use the `memo` too when one is returned.
80
+
81
+ There are no withdrawal or transfer methods: API keys cannot withdraw or transfer funds.
82
+
83
+ ## Amounts
84
+
85
+ Every amount is an exact decimal **string** (`"0.00150000"`), in responses and requests. JS numbers are
86
+ binary floats and cannot hold most decimals exactly, so the SDK **rejects a `number` in any amount field
87
+ before sending** (`InvalidAmountError`). Do arithmetic with a decimal library, for example:
88
+
89
+ ```ts
90
+ import Decimal from "decimal.js"; // or big.js, bignumber.js
91
+ const total = new Decimal(order.price!).times(order.quantity).toFixed();
92
+ ```
93
+
94
+ `isAmount(value)` checks that a string is a plain decimal.
95
+
96
+ ## Errors
97
+
98
+ Every API failure throws a `CexyApiError` (or a subclass) with `status`, `code`, `message`, `details`,
99
+ `fields`, `requestId` and `retryable`. Branch on `code`, never on `message`.
100
+
101
+ | Class | When |
102
+ |---|---|
103
+ | `AuthenticationError` | 401: missing or invalid credentials |
104
+ | `ForbiddenError` | 403: the key lacks a scope (`FORBIDDEN`), or the route is session-only (`API_KEY_NOT_ALLOWED`) |
105
+ | `ValidationError` | 400: see `fields` |
106
+ | `NotFoundError` | 404 |
107
+ | `ConflictError` | 409: `ALREADY_EXISTS`, `IDEMPOTENCY_KEY_CONFLICT`, `CONCURRENT_MODIFICATION` |
108
+ | `UnprocessableError` | 422: `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE`, ... |
109
+ | `RateLimitError` | 429, with `retryAfterMs` |
110
+ | `ServerError` | 5xx |
111
+ | `CexyApiError` | any code this SDK version does not know yet |
112
+
113
+ Local problems use `CexyConfigError`, `InvalidAmountError`, `CexyConnectionError` / `CexyTimeoutError`
114
+ and `OrderStateUnknownError`. `ErrorCode` is a union of the known codes plus `string`, because new codes
115
+ can appear: keep a default branch.
116
+
117
+ ```ts
118
+ try {
119
+ await cexy.trading.placeOrder(order);
120
+ } catch (err) {
121
+ if (err instanceof CexyApiError && err.code === "INSUFFICIENT_FUNDS") console.log(err.details);
122
+ else throw err;
123
+ }
124
+ ```
125
+
126
+ ## Retries and idempotency
127
+
128
+ - Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
129
+ - Retried: network errors, timeouts and responses with `retryable: true`.
130
+ - 429 waits at least `Retry-After` / `details.retry_after_seconds`.
131
+ - GETs retry freely.
132
+ - **Orders:** safety rests on `client_order_id`, not on `Idempotency-Key` (the server does not honour
133
+ that header on `POST /trading/orders`, order cancels or cancel-all). `placeOrder` always sends a
134
+ `client_order_id` (a UUID if you do not set one); it is unique per account and a repeat is refused
135
+ before any funds move. After an ambiguous failure (network error, timeout, 5xx) the SDK first looks the
136
+ order up by that id. If the order exists it is returned with `recovered: true`; only if it does not
137
+ exist is it sent again, with the same id. If even the lookup fails you get `OrderStateUnknownError`:
138
+ check `trading.orderByClientId()` before placing the order again.
139
+ - **Cancels:** `cancelOrder` retries network errors; if a *retry* gets `INVALID_STATE`, the first attempt
140
+ already cancelled the order, so the SDK fetches and returns it. `cancelAll` is naturally repeatable and
141
+ is retried the same way (a retry reports only what it cancelled).
142
+ - **Pool join/exit** send an auto-generated `Idempotency-Key`, reused on every retry; the server honours
143
+ it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key still in flight) is
144
+ retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
145
+ - `onRetry` lets you log retries.
146
+
147
+ Every method takes a last `RequestOptions` argument: `{ signal, timeoutMs, maxRetries, idempotencyKey }`.
148
+
149
+ ## Pagination
150
+
151
+ Histories use opaque cursors. Each listing returns one page (`items`, `has_more`, `next_cursor`), and has
152
+ an async iterator that fetches pages lazily:
153
+
154
+ ```ts
155
+ for await (const order of cexy.trading.iterateOrderHistory({ symbol: "BTC/USDT", limit: 100 })) {
156
+ console.log(order.id, order.status);
157
+ }
158
+ // cap the total: cexy.account.iterateLedger({}, { maxItems: 500 })
159
+ ```
160
+
161
+ ## Rate limits
162
+
163
+ The client has a token-bucket limiter: **100 requests/minute without a key** (the server allows 120/min
164
+ per IP for anonymous calls) and **300/minute with an API key** (the server allows 600/min per key once the
165
+ per-key limits are live). Cancel-all is limited separately to 30/min per account. It adapts
166
+ downwards to `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` (seconds until the window
167
+ resets), and pauses after a 429. Change it with `rateLimit: { requestsPerMinute }`, or turn it off with
168
+ `rateLimit: false`. The limiter is per client instance: share one client.
169
+
170
+ ## WebSocket
171
+
172
+ ```ts
173
+ const cexy = new CexyClient();
174
+ const ws = cexy.websocket(); // wss://api.cexy.io/api/v1/ws
175
+ await ws.connect();
176
+
177
+ ws.on("event", (e) => {
178
+ if (e.type === "ticker.update") console.log(e.channel, e.data);
179
+ });
180
+ await ws.subscribe(["ticker:BTC/USDT", "trades:BTC/USDT"]);
181
+
182
+ const book = await ws.orderBook("BTC/USDT");
183
+ book.on("update", (b) => console.log(b.top.bid, b.top.ask, b.stale ? "stale" : ""));
184
+
185
+ ws.on("reconnected", () => console.log("reconnected and re-subscribed"));
186
+ ws.on("resync", () => {/* refetch anything you derive from events */});
187
+ ```
188
+
189
+ What the client does for you:
190
+
191
+ - Sends `{"op":"ping"}` every 30 s (required: the server closes idle connections) and accepts the server's
192
+ unsolicited pongs. No frame for 75 s means a dead connection and a reconnect.
193
+ - Reconnects with exponential backoff and full jitter, then re-authenticates and re-subscribes everything.
194
+ - Correlates every request with its acknowledgement by `id`: `auth` resolves on `authenticated`
195
+ (and rejects on an `error` with its id, or on timeout), `subscribe` on `subscribed`, `unsubscribe` on
196
+ `unsubscribed`, `ping()` on `pong`.
197
+ - Guards locally: at most 100 subscriptions (extras are returned in `refused`) and 200 messages/minute.
198
+ - Warns once if the server speaks a newer `protocol_version`, and ignores unknown event types.
199
+
200
+ **Order-book rules** (applied by `ws.orderBook()`; follow them if you build your own):
201
+
202
+ 1. Subscribe to `orderbook:{symbol}` first, then take the REST snapshot (its `sequence` is S).
203
+ 2. Drop updates with `sequence <= S`.
204
+ 3. Every update carries the complete top 50 of both sides (`"full": true`) and replaces the previous state.
205
+ There are no deltas. Never merge REST levels deeper than 50 into WebSocket state.
206
+ 4. A sequence gap marks the book `stale` until the next update, which heals it. There is no forced resync.
207
+ 5. Sequences reset when the server restarts: take a fresh snapshot after every reconnect.
208
+ 6. An `error` frame `CONCURRENT_MODIFICATION` with a null `id` means messages were dropped: resync every book and channel (the client emits `resync`).
209
+
210
+ **Private channels** (`orders`, `balances`, `deposits`, `withdrawals`, `account`) need
211
+ `await ws.auth(token)` with a session access token (it resolves with the `user_id` from `authenticated`). **API-key authentication on the WebSocket is not available yet**: with an API key,
212
+ use public channels and poll REST for private state. If the session is revoked, the client emits
213
+ `authLost`; public channels keep working.
214
+
215
+ In Node the client uses the optional `ws` package when installed (so it can send the SDK User-Agent),
216
+ otherwise the global `WebSocket` (Node 22+, browsers).
217
+
218
+ ## Browsers
219
+
220
+ The SDK runs in browsers, but **never put API keys in a browser bundle**: anyone can read them. Call private
221
+ endpoints from a server. Browsers do not let scripts set `User-Agent`, so the SDK does not send one there.
222
+
223
+ ## Security
224
+
225
+ - API keys **cannot withdraw or transfer funds**, whatever their scopes.
226
+ - Use a **read-only** key unless you need to trade, and restrict keys to your IPs (`allowed_ips`).
227
+ - Credentials go only in the `X-API-Key` / `X-API-Secret` headers and only on private endpoints; never in URLs.
228
+ - Only `https://` base URLs and `wss://` WebSocket URLs are accepted. `allowInsecure: true` permits
229
+ `http://` / `ws://` solely for `localhost`, `127.0.0.1` or `::1` (local test servers).
230
+ - The SDK redacts the secret from `toString()`, `util.inspect`, `JSON.stringify` and error messages.
231
+ - Keep keys in environment variables or a secret manager, not in code.
232
+
233
+ Report vulnerabilities as described in [SECURITY.md](SECURITY.md).
234
+
235
+ ## For tool builders
236
+
237
+ The package exports its building blocks: the `OPERATIONS` table (method, path, auth and scope for each of the
238
+ 40 operations), all model types, the error classes, `paginate()`, `RateLimiter`, the `Authenticator`
239
+ interface (HMAC signing will plug in here) and `userAgentSuffix` to identify your tool.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ npm ci
245
+ npm run generate # regenerate src/generated/schema.ts from ../cexy-api-spec/spec/openapi.sdk.json
246
+ npm run lint && npm run typecheck && npm test
247
+ npm run build
248
+ CEXY_LIVE_TESTS=1 npm run test:live # optional: 3 anonymous GETs against api.cexy.io
249
+ ```
250
+
251
+ Tests read the shared conformance cases from a `cexy-api-spec` checkout next to this repo
252
+ (override with `CEXY_API_SPEC_DIR`).
253
+
254
+ ## License
255
+
256
+ MIT, see [LICENSE](LICENSE).