npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@koniverse/trader

v0.5.0

Published

Self-contained Layer-1 direct-client SDK for crypto-exchange auto-trading bots. Direct port over ccxt; no platform entanglement.

Readme

@koniverse/trader

A self-contained npm SDK for writing crypto-exchange auto-trading bots. Imported into your own process, holds one ccxt.pro client, and gives you a direct client.* surface — watchOHLCV, placeMarket, bracket, setSltp, sweepBrackets, balance, … — without bringing the Koni Meta Trader platform along for the ride.

  • Install: npm i @koniverse/trader — that is the whole install. ccxt is a dependency of this package and is re-exported from it, so you never declare it yourself.
  • Use: load credentials from .env, build a client, drive the methods yourself.
  • Reach: any venue ccxt can construct — 105 ids across the pro and REST registries. Two of them are certified; the rest connect and read all the same.
  • Phase 1 (TS-only): one direct client, no backtest, no marketplace. The same surface, identically named, will ship on PyPi in a later phase.

Quickstart — a defineBot trading one closed bar

1. Install

npm i @koniverse/trader

One package. ccxt comes with it as this package's only dependency, and is re-exported — so client.exchange, ccxt's error classes and ccxt.pro are all reachable without a second install, and they reach the same copy the client was built from. See Holding a specific ccxt release if you need one this package has not floored yet.

2. .env

# A CEX: Binance USDⓈ-M demo keys
KONI_BOT_API_KEY=…
KONI_BOT_SECRET=…

# OKX / KuCoin / Bitget also want the passphrase (ccxt calls it `password`; we do not)
# KONI_BOT_PASSPHRASE=…

# A DEX: hyperliquid signs with a wallet key instead
# KONI_BOT_WALLET_ADDRESS=0x…
# KONI_BOT_PRIVATE_KEY=0x…

# Your bot's own parameters live here too. Declare them once in bot.manifest.json
# and derive the config from that declaration — see (k) below.
BOT_SYMBOL=BTC/USDT:USDT
BOT_EMA_FAST=9

Which variables a venue needs is not a list this package maintains — ask it. The answer is a Result, because an id ccxt cannot construct is a refusal rather than an empty array:

import { requiredCredentials, isRefusal } from '@koniverse/trader';

const need = requiredCredentials('okx');
if (!isRefusal(need)) need.value; // ['KONI_BOT_API_KEY','KONI_BOT_SECRET','KONI_BOT_PASSPHRASE']

requiredCredentials('hyperliquid'); // → ['KONI_BOT_PRIVATE_KEY','KONI_BOT_WALLET_ADDRESS']
requiredCredentials('nosuchvenue'); // → Refusal { code: 'unsupported', … }

mode is live, testnet or demo, and the SDK verifies the switch took: a venue that cannot serve the mode you asked for refuses, rather than handing back a production client. Not every venue has every environment — hyperliquid has testnet but no demo, binanceusdm the reverse.

3. bot.ts

import { createClientFromEnv, defineBot, isRefusal } from '@koniverse/trader';

const built = await createClientFromEnv('binanceusdm', { mode: 'demo' });
if (isRefusal(built)) {
  console.error(`connect refused: ${built.code} (${built.scope})`);
  process.exit(1);
}
const client = built.value;

let bars = 0;
const bot = defineBot(client, {
  symbols: ['BTC/USDT:USDT'],
  timeframes: ['1m'],
  sweep: false,                  // this loop only reads; do not reap bracket legs
  onBoot: (_event, client) => {
    bot.log('boot', { venue: client.id, mode: client.mode, registry: client.registry });
  },
  onBar: ({ bar, symbol }) => {
    bot.log('bar', { symbol, close: bar.close });
    if (++bars >= 1) void bot.stop(); // one closed bar is enough for a smoke
  },
});

const summary = await bot.run();   // run() closes the client on the way out
if (isRefusal(summary)) {
  console.error(`run refused: ${summary.code} (${summary.scope})`);
  process.exit(1);
}
console.log('done', summary.value.counts);

Run with tsx bot.ts (or node after tsc). The first closed bar arrives, you log it, and the loop shuts down cleanly.

What the SDK supports

Every call returns Result<T> — either { ok: true, value } or a Refusal. Nothing here throws for a venue condition.

(a) Connection & lifecycle

| Call | What it does | |---|---| | createClientFromEnv(exchangeId, { mode }) | The single ccxt construction site. Any id ccxt knows — pro registry first, plain REST second. mode is verified by its effect, not by whether a method exists. | | requiredCredentials(exchangeId) | The KONI_BOT_* variables this venue needs, derived from ccxt's own map in our spelling. | | loadCredentials() / verifyCredentials() | Env loader, and a signed probe that proves account control (hyperliquid: the submitted wallet must be the agent key's master). | | client.id / client.mode / client.registry | registry: 'rest' means the venue publishes no ccxt.pro class, so every watch* refuses feed_unavailable — distinguishable from "this feed is down". | | client.exchange | The raw ccxt slice, for anything this surface does not cover. Typed as ccxt's own Exchange — reach the class through the re-export (import { ccxt } from '@koniverse/trader'), never a second install. | | ccxt | ccxt itself, re-exported. ccxt.Exchange, ccxt.pro, ccxt.RateLimitExceeded, every exchange class — the SDK's own copy, so client.exchange instanceof ccxt.Exchange is true and your rate-limit config applies to the requests that actually go out. | | client.close() | Idempotent. Stops every registered stream, then tears down sockets and timers. |

(b) Market data — REST

| Call | Notes | |---|---| | ohlcv(symbol, tf, opts?) | Closed candles, oldest-first. The forming candle is excluded. | | ticker(symbol) / tickers(symbols?) | bid/ask are null where the venue publishes none. A bulk request over the venue's measured symbol cap refuses rather than truncating into a partial answer. | | orderbook(symbol, { limit }) | limit is a promise this SDK keeps: passed to the venue and the result truncated, because venues measurably disagree about honouring it. | | trades(symbol, opts?) | The public tape. order and fee are null on every row. | | markPrice(symbol) | Carries the instant it was observed — a naked number cannot say how stale it is. Derived from the venue's bulk read where the singular under-reports. | | fundingRate(symbol) / fundingRates(symbols?) / fundingRateHistory(symbol, opts?) | Contract markets only; a spot symbol refuses before the wire naming its market kind. A history row carrying no rate is dropped, not zero-filled. | | market(symbol) / markets() | Tick, step, min quantity, min notional — as decimal strings and numbers. |

(c) Market data & account — realtime

Nine watch* feeds over one driver and one reconnect policy. All synchronous: a feed the venue does not publish is refused before any socket opens, so you branch before committing to a for await.

| Public | Private | |---|---| | watchOHLCV(symbol, tf, opts?) (closed bars) | watchOrders(symbol?, opts?) | | watchTicker(symbol, opts?) | watchMyTrades(symbol?, opts?) | | watchTrades(symbol, opts?) | watchPositions(symbols?, opts?) | | watchOrderBook(symbol, opts?) | watchBalance(opts?) | | watchMarkPrice(symbol, opts?) | |

The eight Stream<T> feeds deliver frames and gaps on one iterator:

const feed = client.watchOrders('BTC/USDT:USDT');
if (isRefusal(feed)) return;
for await (const ev of feed.value) {
  if (ev.kind === 'gap') {
    // The socket dropped and came back. The venue's own error, a monotonic counter, how many
    // reconnects it took and how long the feed was down. The SDK re-fetches NOTHING.
    console.warn('gap', ev.gap, ev.downMs, ev.cleanClose, ev.reason);
    continue;
  }
  ev.data; // an Order — the SAME object `fetchOrder` returns, mapped eagerly inside the tick
}
  • A gap is reported, not handled. What to re-read is your call: the SDK runs no REST resync, buffers nothing pending one, and reconciles nothing. A clean close still emits a gap — suppressing it because the close was polite would be the SDK deciding which interruptions you are allowed to know about.
  • No replica of venue state. An order going filled → open → filled arrives three times, in order. A duplicate after a resubscribe is delivered. There is no keyed collection anywhere behind these feeds, and a build-failing structural test keeps it that way.
  • Two concurrent streams over the same (method, symbol) are refused — ccxt.pro's delta cursor is consuming, so two pumps would each see a fraction of the frames and neither would know.

close() also clears the timers the venue scheduled on itself. binanceusdm's user-data stream arms a 20-minute listen-key keep-alive through ccxt's delay(), which discards the handle — so a bounded script that opened a private feed used to finish its work and then sit on a live event loop forever, looking exactly like a hung bot. close() owns those handles now, and a script that is done exits on its own.

(d) Account, positions & orders — reads

| Call | Notes | |---|---| | balance(currency?) | Defaults to the venue's settle currency. A currency the account holds nothing in refuses; it is never reported as 0. | | positions(symbols?) | side from the sign of the size, never the venue's bucket. qty >= 0, flat rows dropped. | | openOrders(symbol?) | Resting orders including conditionals — the regular ∪ conditional union. An unreadable conditional sweep refuses rather than handing back the regular set as if it were everything. | | openOrdersByClass(symbol?) | The same enumeration with the classes kept apart and conditionalCovered stated — the degraded view, asked for by name. | | protection(symbol) | The guards resting on a symbol, plus coverage: 'observed' \| 'unknown'. | | closedOrders(symbol?, opts?) / myTrades(symbol?, opts?) | Pagination passes through to the venue. | | fetchOrder(id, symbol) / fetchOrderByClientId(coid, symbol) | Reads from whichever endpoint holds it, retrying the conditional one only on not_found. The per-venue client-id param spelling is measured, never inferred. |

(e) Cashflow — the money that moves outside the fills

ledger(opts?) · tradingFees() · tradingFee(symbol) · fundingHistory(symbol?, opts?) — behind four independent capability gates, so a venue missing one still serves the others.

Two facts the SDK states rather than smoothing over:

  • tradingFees() (plural) on binanceusdm comes from ccxt's own static fee table indexed by the account's feeTier — one pair stamped onto every linear symbol. tradingFee(symbol) reads the account's real commission rate. Same shape, two provenances.
  • ledger() is a superset of fundingHistory() on binanceusdm — the same income endpoint, one of them filtered — so summing the two double-counts funding. The SDK does not de-duplicate: that is an accounting decision and it is yours.

(f) Order placement & management — writes

| Call | Notes | |---|---| | placeMarket(symbol, side, qty, opts?) | opts.price supplies the minNotional reference on a venue with no market primitive. | | placeLimit(symbol, side, qty, price, opts?) | The full admission predicate runs before the wire. | | placeTrigger(symbol, side, qty, { triggerPrice, when, type, price? }, opts?) | A conditional that OPENS — the breakout shape. when: 'above' \| 'below' is required: the SDK reads no price to infer it, because that inference races the market and inverts on a short. Carries neither reduceOnly nor a close flag, and an explicit opts.reduceOnly is refused, not dropped. | | placeTrailing(symbol, side, qty, { callbackPercent \| callbackAmount, activationPrice? }, opts?) | The venue's own trail, or a refusal naming what it does publish. Nothing is emulated: a cancel-and-replace loop has different failure modes and stops when your process does. | | bracket(symbol, side, qty, { stop?, take? }, opts?) | Enter with protection attached — native, emulated split-leg, or refused before the wire. A Refusal means the entry did not happen. Check reapedBy: 'sweeper' means you owe sweepBrackets(). | | setSltp(symbol, request, opts?) | Arm SL/TP on an open position, reporting the leg it replaced. | | sweepBrackets(symbol?) | Cancel the emulated legs that no longer guard anything. Explicit by design — this SDK never cancels an order on a schedule you did not choose. | | amend(orderId, symbol, changes) | result.mode says which of two things the venue did: 'amended' kept the id and all its state; 'recreated' destroyed and rebuilt it, so the SDK re-sent reduceOnly, the leg class and the kb- bracket lineage for you. A field this venue's amend cannot carry is refused before the wire. | | cancel(orderId, symbol) / cancelAll(symbol?) | cancel answers a CancelOutcome: a conditional's cancel ack is a bare confirmation naming no side, so order is null and the cancel still succeeded. cancelAll refuses on an unreadable conditional set — "cancelled everything" while a stop-loss is still resting is the one lie this SDK will not tell. (A free cancelAccepted(api, …) → Result<true> export exists for callers that only want the fact.) | | closePosition(symbol, { side?, hedged? }) | side names which position; hedged states the account's mode — two facts, not one. An un-named close on a two-sided symbol refuses: closing the wrong one doubles the exposure. Sized from that position's freshly-read quantity, through the same admission predicate a placement uses. |

(g) Derivatives account configuration

positionMode() / setPositionMode(mode) · marginMode(symbol) / setMarginMode(symbol, mode) · leverage(symbol) / setLeverage(symbol, n).

Every setter returns one shape:

interface ConfigChange<T> {
  requested: T;
  applied: boolean;   // did the venue CHANGE something? false = it was already there (a success)
  verified: boolean;  // did the SDK read it back and find it equal?
  mode: T;            // what is in force, as far as the SDK can state
}

A venue that cannot report a mode is refused, not assumed. A change blocked by an open position or a resting order answers state_conflict carrying the venue's own sentence — a well-formed request the account's state forbids, which is a different remedy from bad_request.

(h) Precision & safety

roundPrice(symbol, px, intent, side?) · roundAmount(symbol, qty) · checkOrder(symbol, qty, px).

Synchronous, so they sit inside a bar handler. Quantities round down, always. checkOrder is the same admission predicate the placements run — a sub-minimum order is refused, never nudged up. Arithmetic under the boundary is scaled-integer, not float.

(i) The optional run-loop — defineBot

defineBot(client, definition){ run, stop, bars, log, every, after, clearTimer }, with onBoot / onBar / onOrder / onFill / onTimer / onError / onShutdown, declared requires resolved to a refuse-to-boot, warmup applied to history rather than dispatched, one FIFO with a fixed dispatch order, timers, structured JSON logging, and SIGINT/SIGTERM shutdown.

Nothing in (a)–(h) imports it. The imperative client is complete on its own; the loop depends on the client, never the reverse. It is also frozen: it stays because it shipped, and it takes no new features.

(j) The in-doubt write

A refusal carrying reachedVenue: 'unknown' means the request may or may not have landed, and a blind retry can double-submit. There is no resolveInDoubt wrapper, because you set the clientOrderId and the answer is six lines of your own source:

const placed = await client.placeLimit(symbol, 'buy', qty, px, { clientOrderId: coid });
if (isRefusal(placed) && placed.reachedVenue === 'unknown') {
  const found = await client.fetchOrderByClientId(coid, symbol);
  if (!isRefusal(found)) { /* it LANDED — do not resend */ }
  else if (found.code === 'not_found') { /* absent — a retry is safe */ }
  else { /* the lookup itself failed — this is evidence of neither */ }
}

Restart is the same picture with no magic: openOrders() + positions() + myTrades(since). What counts as "clean" is your strategy's call — grid, market-making and delta-neutral bots hold stopless positions on purpose, so the SDK does not have an opinion about it.

(k) bot.manifest.json — your parameters as data

Declare your parameter set once, in a JSON file beside your source, and derive the config from it. The point is not the file format: it is that the declaration and the consumption are the same artifact, so a deployment platform can render a form from the same thing your bot parses — and the two cannot disagree.

{
  "manifestVersion": "1",
  "bot": { "id": "ema-crossover", "displayName": "EMA crossover", "sdkRange": "^0.4.0" },
  "parameters": [
    { "name": "BOT_SYMBOL", "type": "string", "label": "Symbol", "required": true },
    { "name": "BOT_EMA_FAST", "type": "integer", "label": "Fast EMA", "required": false,
      "default": 9, "min": 1 },
    { "name": "BOT_EMA_SLOW", "type": "integer", "label": "Slow EMA", "required": true, "min": 2,
      "description": "must be greater than BOT_EMA_FAST" },
    { "name": "BOT_NOTIONAL_USDT", "type": "number", "label": "Notional", "required": true,
      "exclusiveMin": 0 },
    { "name": "BOT_TIMEFRAME", "type": "enum", "label": "Timeframe", "required": true,
      "enum": ["1m", "5m", "15m"] },
    { "name": "KONI_BOT_API_KEY", "type": "string", "label": "API key", "required": true,
      "secret": true }
  ]
}
import { readFileSync } from 'node:fs';
import { defineConfig, isRefusal } from '@koniverse/trader';

const manifest = JSON.parse(readFileSync(new URL('../bot.manifest.json', import.meta.url), 'utf8'));

const config = defineConfig(manifest, process.env);
if (isRefusal(config)) {
  console.error(config.message);
  process.exit(1);       // YOUR boundary decides what is fatal — the SDK returns, it never exits
}
config.value.BOT_EMA_FAST; // 9, frozen, and a number rather than a string

| Call | Notes | |---|---| | validateManifest(value) | Result<BotManifest> over anything JSON.parse produced. Refuses an unknown field, a default alongside required: true, a default breaking its own type / bound / enum, min above max, a duplicate name, and a name outside ^[A-Z][A-Z0-9_]{0,63}$ or reserved by the host runtime (PATH, LD_PRELOAD, SERVICE_*). | | defineConfig(manifest, env) | Validates the manifest first — an author error says so, rather than reading as a bad value someone typed — then resolves env[name]default → refuse-if-required, coerces, bounds-checks, and returns a frozen record. |

Four things worth knowing before you write one:

  • A configuration failure names every offending parameter at once. Fixing a deploy should cost one iteration, not one per mistake.
  • boolean accepts exactly "true" and "false", case-sensitively. "1", "yes", "on" and "False" are all refused, and the refusal says so. Under a truthiness-shaped coercion "False" is true, and a bot that inverts a flag over a capital letter fails in the worst way available. For the same reason integer refuses "3.5" instead of truncating it to 3.
  • Cross-field relations stay yours. The format is flat and per-field on purpose: BOT_EMA_SLOW > BOT_EMA_FAST is not expressible, so keep that check in your bot after the call and write the rule into the parameter's description — that is what a form shows the operator.
  • secret: true is mandatory on a KONI_BOT_* name, and the name must be one the SDK actually computes (KONI_BOT_API_KEY, not KONI_BOT_APIKEY). The flag is routing, not decoration: it is what tells a deployment platform to hold the value for one call instead of storing it. A secret's value never appears in a refusal — only its name.

There is no pattern in v1 (regex dialects disagree across the languages this SDK plans to ship in, and the platform runs it in a browser over operator input), no type inference from the manifest, and no loader — you read the file, because a helper that throws when it is missing is a dependency you did not ask for.

Certified venues

Certification is a confidence tier, not an entry gate. Every venue ccxt can construct connects, reads, and writes whatever exchange.has declares. A CERTIFIED venue is one somebody put through the twelve-dimension procedure live and committed the evidence for; that is what unlocks the writes needing a curated fact (conditional order classes, bracket shape, trigger basis) and nothing else. A venue absent from the list loses the label and nothing more.

import { CERTIFIED_VENUES, venueTruth, CCXT_VERSION } from '@koniverse/trader';

CERTIFIED_VENUES;            // ['binanceusdm', 'hyperliquid']
venueTruth('okx') === null;  // true: connects and reads, no curated writes
CCXT_VERSION;                // the ccxt version actually resolved in your tree

The evidence ships with the package under certification/<exchangeId>.json, one file per venue, carrying the mode, symbol, ccxt version and one line of observation per dimension. Both records were measured 2026-08-04 against ccxt 4.5.70 and re-run on the same day, so they share a vintage: a list that certifies its second venue against code its first was never re-measured under is a list nobody should trust.

That 4.5.70 is also this package's ccxt floor, which is not a coincidence — the evidence and the shipped dependency move together. ^4.5.70 still admits later 4.x releases, so CCXT_VERSION can legitimately read higher than the certified one; when it does, the certification records describe an older release than the code you are running, and the difference is yours to judge. Pin with an override (below) if that matters for your venue.

The two are deliberately unalike — an api-key pair against a wallet key, a market primitive against none, a venue-managed close flag against reduceOnly as the only channel, a 36-character client id against 128-bit hex, a separate conditional endpoint against trigger orders on the regular set, readable margin and leverage against neither, a bulk cancel against none. binanceusdm measures twelve of twelve; hyperliquid measures eleven with margin_and_leverage explicitly unsupported — kept distinct from unknown, because recording an unmeasured thing as absent claims a fact about the venue that is really a fact about us.

Refusal codes

Fifteen, closed, and every one of them is something this SDK can actually emit:

unsupported · requires_auth · risk_refused · symbol_not_owned · rounding_impossible · shutting_down · feed_unavailable · bad_request · auth_error · rate_limited · venue_unavailable · timeout · not_found · state_conflict · unknown

reachedVenue is three-state from day one — true, false, 'unknown' — because a transport timeout is genuinely the in-doubt case.

Reference bot

A complete, runnable skeleton is at examples/reference-bot.ts. It runs one bounded write cycle against any venue, in a non-live mode:

preflight the credentials → connect and report the registry → warn if the venue is not certified → read the baseline through openOrdersByClass (coverage stated) → ask the venue for positionMode / marginMode / leverage / tradingFee → open watchOrders beside the loop → read market context (markPrice, fundingRate, orderbook) → size at the venue's own minimum → place a MARKET bracket (long, 3% SL, 3% TP) → resolve an in-doubt write via fetchOrderByClientId → confirm the position and then protection() → probe amend on the take-profit leg → close side-scoped, poll until flat, sweepBrackets, and assert zero residue from fresh reads.

# From the sdk/typescript/trader directory. Demo/testnet keys — never live ones.
export KONI_BOT_API_KEY=… KONI_BOT_SECRET=…
pnpm smoke:e2e                                        # binanceusdm demo, BTC/USDT:USDT
tsx examples/reference-bot.ts --venue=hyperliquid --mode=testnet
tsx examples/reference-bot.ts 'ETH/USDT:USDT'
tsx examples/reference-bot.ts --hedged                # override the venue's own positionMode read

--mode accepts demo and testnet only; live is refused, because this file opens a real position and a typo that trades real money is not a class of accident worth leaving reachable.

It imports from the SDK barrel (@koniverse/trader) — the exact public package root, never ../src — refuses to start on a non-empty or partially-covered baseline, and runs its cleanup while the client is still open, because bot.run() closes it on the way out.

Restart caveat: process memory does not survive a restart. This bot rebuilds its baseline from a fresh read and stops without touching anything if that baseline is non-empty. Full reconcile is the author's, by design — see (j) above.

The model

  • The author owns the loop. defineBot is optional — the imperative client (client.watchOHLCV, client.placeMarket, client.bracket, …) is complete on its own.
  • Refusals are values, not exceptions. A call that cannot go through returns a Refusal { ok: false, code, scope, reachedVenue, message }. throw is reserved for programmer error. isRefusal(x) narrows it; client.* returns Result<T> = { ok: true, value: T } | Refusal.
  • One ccxt construction site. createClientFromEnv is the only place ccxt is wired up. Everything else takes a structural slice — so a fake can stand in for tests, and a future Python port can re-derive the same surface without sharing code.
  • ccxt is a dependency, and it is re-exported. One install, one copy, one Exchange class — which is what makes instanceof answer correctly and your rate-limit settings apply to the requests that go out. ccxt still ships weekly breaking releases; the ^4.5.70 floor is set at the release the venue certifications were measured against, and an override is how you hold a different one.
  • The SDK holds no state on your behalf. No restart memory, no local files, no database, no world-view rebuilt after a crash. It feeds data from the venue and sends orders to it; the bot's logic, state and lifecycle are yours.
  • Curated venue truth is an OVERRIDE, never a gate. exchange.has is the default capability surface; a descriptor entry may only correct a flag a live measurement caught being wrong. No file but venue-truth.ts may branch on an exchange id, and a test fails the build if one does.

Compatibility

Since the published 0.1.0, two signatures changed. Both are one-property migrations:

| Call | 0.1.0 | Now | Why | |---|---|---|---| | setLeverage(symbol, n) | Result<true> | Result<ConfigChange<number>> | r.valuer.value.mode. The applied / verified split is what makes a write-vs-confirm difference visible to a bot. | | cancel(orderId, symbol) | Result<Order> | Result<CancelOutcome> | r.valuer.value.order, plus a null case that previously arrived as a refusal you had to special-case anyway. A conditional's cancel ack names no side, so there is no row to return — and the old shape therefore refused a cancel the venue had confirmed. |

Order.role also reads differently: a conditional that opens exposure now reads 'entry', and only one that closes reads 'stop' / 'take'. That is a correction rather than a break — no 0.1.0 call could create a conditional entry, so the previous reading was reachable only through client.exchange or the venue's own UI, and it was wrong in both. It fixes protection(), which counts exactly the 'stop' / 'take' rows and would otherwise present a breakout entry as the thing guarding the position.

reconcile() never shipped in a published version and cannot break a consumer.

ccxt moved from a peer to a dependency

Through 0.2.0, ccxt was a peerDependency at >=4.4 <5 and you installed it yourself. It is now a plain dependency at ^4.5.70 and is re-exported from this package. (0.3.0 shipped that dependency at ^4.5.59; 0.4.0 raises the floor to the release the certifications are now measured at.)

If you already declare ccxt in a compatible range, nothing breaks. Your manager sees two requirements for one package and resolves a single copy that satisfies both — the same deduplication that happens for any shared transitive dependency. You can leave the row where it is; dropping it just removes a decision you no longer have to make. The reason to drop it is that a row pinned outside ^4.5.70 gives you two copies, and two copies is the failure mode this change exists to remove: instanceof answers false about objects that are instances, each copy carries its own rate-limiter, and Exchange stops being assignable to Exchange.

Holding a specific ccxt release

^4.5.70 floors the version at the release the certifications were measured against and admits later 4.x. If you need a different one — a fix newer than the floor, or an older release you have qualified — use your manager's override rather than a direct dependency. An override applies to every copy in the tree, which is what keeps it at one; a direct dependency at a conflicting range adds a second.

// npm / bun — package.json
{ "overrides": { "ccxt": "4.5.72" } }

// pnpm — package.json
{ "pnpm": { "overrides": { "ccxt": "4.5.72" } } }

// yarn (berry and classic) — package.json
{ "resolutions": { "ccxt": "4.5.72" } }

Below the ^4.5.70 floor is untested territory: the venue certifications were measured at that release and the package's own suite runs against it. Above it is ordinary ccxt upgrade risk — read their changelog, and remember CCXT_VERSION tells you what actually resolved.

Shape of the package

src/
├── index.ts              barrel (re-exports everything below)
├── domain.ts             Order, Position, Account, MarketMeta, ConfigChange, Protection, …
├── scalars.ts            Side, PosSide, Ms, Timeframe, Dec, PositionMode, MarginMode, …
├── refusal.ts            isRefusal, refuse, ok, Result<T>
├── decimal.ts            the scaled-integer core
├── precision.ts          scaled-integer arithmetic under the precision boundary
├── venue-truth.ts        per-venue curated facts — an OVERRIDE, never a gate
├── venue-certification.ts the twelve dimensions, the tier, and CERTIFIED_VENUES
├── capabilities.ts       exchange.has gate + the refusal-taxonomy
├── ccxt-errors.ts        taxonomy shared by the read + write paths
├── ccxt-slice.ts         the structural slice of ccxt this package depends on
├── credentials.ts        loadCredentials, requiredCredentials
├── connect.ts            createClientFromEnv — the single construction site, and the
│                         `ccxt` re-export (a pass-through from the one importer, not a second site)
├── normalize.ts          ccxt rows → domain objects, one mapper per type
├── market-meta.ts        tick / step / minQty / minNotional, per symbol
├── market-data/{rest,stream}.ts
├── reconnect.ts          one reconnect policy behind every watch*
├── stream-driver.ts      one pump behind every watch*, gaps reported and never resynced
├── private-stream.ts     watchOrders / watchMyTrades / watchPositions / watchBalance
├── account.ts            balance / positions / closedOrders / myTrades / protection
├── open-orders.ts        the regular ∪ conditional union, and the degraded by-class view
├── account-config.ts     positionMode / marginMode / leverage, read and set
├── cashflow.ts           ledger / tradingFees / tradingFee / fundingHistory — four gates,
│                         no de-duplication
├── orders.ts             placeMarket / placeLimit / cancel / cancelAll / closePosition
├── order-lookup.ts       fetchOrder / fetchOrderByClientId — the in-doubt answer
├── amend.ts              amend — 'amended' or 'recreated', stated per venue
├── bracket.ts            bracket / setSltp / sweepBrackets (three modes, none silent)
├── trigger.ts            placeTrigger (a conditional that OPENS) / placeTrailing
├── manifest.ts           validateManifest / defineConfig — a parameter set declared once,
│                         and the frozen config derived from that declaration
└── bot.ts, bot-account.ts, bot-timers.ts, bot-requires.ts
                          the optional defineBot run-loop

License

MIT — see LICENSE.