@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 +21 -0
- package/README.md +256 -0
- package/dist/index.cjs +1802 -0
- package/dist/index.d.cts +3929 -0
- package/dist/index.d.ts +3929 -0
- package/dist/index.js +1752 -0
- package/package.json +84 -0
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).
|