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

@plutarc/bybit

v0.1.0

Published

Fully-typed Bybit V5 REST and WebSocket client for TypeScript/JavaScript

Readme

@plutarc/bybit

A fully-typed Bybit V5 REST and WebSocket client for TypeScript/JavaScript. Zero runtime dependencies — uses only Node.js/Bun built-ins.

Table of Contents

Features

  • Complete V5 REST API — Market, Order, Position, Account, and Asset endpoints with full TypeScript types
  • Real-time WebSocket — Public (per-category) and private WebSocket clients with typed event emitters
  • Zero dependencies — Uses only node:crypto and node:events built-ins
  • Fully typed — Every request parameter and response field has TypeScript interfaces
  • Unified V5 API — Single client for spot, linear, inverse, and option markets via category parameter
  • Rate limiting — Built-in rolling window rate limiter that tracks X-Bapi-Limit-Status
  • Auto-retry — Exponential backoff with jitter for retryable errors (429, 5xx)
  • Auto-reconnect — WebSocket reconnection with exponential backoff
  • Ping/pong — Automatic JSON heartbeat to keep WebSocket connections alive
  • Delta parser — Generic WebSocket state management with orderbook convenience store
  • Authentication — HMAC-SHA256 signing for both REST and WebSocket
  • Testnet support — Single flag to switch between production and testnet
  • Dual format — Ships ESM and CJS builds with full declaration files

Installation

# bun
bun add @plutarc/bybit

# npm
npm install @plutarc/bybit

# pnpm
pnpm add @plutarc/bybit

Quick Start

REST Client

import { BybitClient } from "@plutarc/bybit";

const client = new BybitClient({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  testnet: true, // use testnet for development
});

// Get instrument info
const instruments = await client.market.getInstrumentsInfo({
  category: "linear",
});

// Get tickers
const tickers = await client.market.getTickers({
  category: "linear",
  symbol: "BTCUSDT",
});

// Place a limit order
const order = await client.order.place({
  category: "linear",
  symbol: "BTCUSDT",
  side: "Buy",
  orderType: "Limit",
  qty: "0.01",
  price: "50000",
  timeInForce: "GTC",
});

// Get wallet balance
const wallet = await client.account.getWalletBalance({
  accountType: "UNIFIED",
});

Public WebSocket

Bybit public WebSocket streams are per-category — each BybitPublicWs instance connects to one market type.

import { BybitPublicWs } from "@plutarc/bybit";

const ws = new BybitPublicWs({
  category: "linear", // "spot" | "linear" | "inverse" | "option"
  testnet: true,
});

// Subscribe to real-time trades — fully typed callback
ws.on("publicTrade", (type, data, topic) => {
  for (const trade of data) {
    console.log(`${trade.S} ${trade.v} @ ${trade.p}`);
  }
});

// Subscribe to orderbook updates
ws.on("orderbook", (type, data, topic) => {
  console.log(`Orderbook ${type}: ${data.b.length} bids, ${data.a.length} asks`);
});

// Subscribe to 1-minute klines
ws.on("kline", (type, data, topic) => {
  for (const candle of data) {
    console.log(`O:${candle.open} H:${candle.high} L:${candle.low} C:${candle.close}`);
  }
});

// Start subscriptions and connect
ws.subscribe([
  "publicTrade.BTCUSDT",
  "orderbook.50.BTCUSDT",
  "kline.1.BTCUSDT",
]);
ws.connect();

Private WebSocket

import { BybitPrivateWs } from "@plutarc/bybit";

const ws = new BybitPrivateWs({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  testnet: true,
});

// Listen for authentication success
ws.on("authenticated", () => {
  console.log("Authenticated — subscribing to private topics");
  ws.subscribe(["order", "position", "wallet", "execution"]);
});

// Order updates
ws.on("order", (data) => {
  for (const order of data) {
    console.log(`Order ${order.orderId}: ${order.orderStatus}`);
  }
});

// Position updates
ws.on("position", (data) => {
  for (const pos of data) {
    console.log(`${pos.symbol}: size=${pos.size} pnl=${pos.unrealisedPnl}`);
  }
});

// Execution (fill) updates
ws.on("execution", (data) => {
  for (const exec of data) {
    console.log(`Fill: ${exec.execQty} @ ${exec.execPrice}`);
  }
});

ws.connect();

REST API Reference

Client Options

const client = new BybitClient({
  apiKey: "...",               // Optional — required for authenticated endpoints
  apiSecret: "...",            // Optional — required for authenticated endpoints
  testnet: false,              // Use testnet (default: false)
  baseUrl: "https://...",      // Custom base URL (overrides testnet flag)
  recvWindow: 5000,            // Receive window in ms (default: 5000)
  rateLimitPer5Seconds: 600,   // Self-throttle limit (default: 600)
  maxRetries: 3,               // Max retries for retryable errors (default: 3)
  baseRetryDelayMs: 500,       // Base delay for exponential backoff (default: 500)
  requestTimeoutMs: 15000,     // Request timeout (default: 15000)
  logger: console,             // Custom logger (default: silent)
});

Market

All market endpoints are public — no API key required.

// Get instrument info
const instruments = await client.market.getInstrumentsInfo({
  category: "linear",
  symbol: "BTCUSDT",
});

// Get tickers
const tickers = await client.market.getTickers({
  category: "linear",
  symbol: "BTCUSDT",
});

// Get orderbook
const book = await client.market.getOrderbook({
  category: "linear",
  symbol: "BTCUSDT",
  limit: 50,
});

// Get klines (candlesticks)
const klines = await client.market.getKline({
  category: "linear",
  symbol: "BTCUSDT",
  interval: "60",
  limit: 100,
});

// Get recent trades
const trades = await client.market.getRecentTrade({
  category: "linear",
  symbol: "BTCUSDT",
  limit: 50,
});

// Get funding rate history
const funding = await client.market.getFundingHistory({
  category: "linear",
  symbol: "BTCUSDT",
});

// Get mark price kline
const markKline = await client.market.getMarkPriceKline({
  category: "linear",
  symbol: "BTCUSDT",
  interval: "60",
});

// Get index price kline
const indexKline = await client.market.getIndexPriceKline({
  category: "linear",
  symbol: "BTCUSDT",
  interval: "60",
});

// Get open interest
const oi = await client.market.getOpenInterest({
  category: "linear",
  symbol: "BTCUSDT",
  intervalTime: "1h",
});

// Get risk limit
const risk = await client.market.getRiskLimit({ category: "linear" });

Order

All order endpoints require authentication. Every method takes a category parameter.

// Place an order
const order = await client.order.place({
  category: "linear",
  symbol: "BTCUSDT",
  side: "Buy",
  orderType: "Limit",
  qty: "0.01",
  price: "50000",
  timeInForce: "GTC",
});

// Amend an order
const amended = await client.order.amend({
  category: "linear",
  symbol: "BTCUSDT",
  orderId: "order-id",
  price: "51000",
});

// Cancel an order
const cancelled = await client.order.cancel({
  category: "linear",
  symbol: "BTCUSDT",
  orderId: "order-id",
});
// or by client order ID
const cancelled = await client.order.cancel({
  category: "linear",
  symbol: "BTCUSDT",
  orderLinkId: "my-order-1",
});

// Get open orders
const open = await client.order.getOpen({
  category: "linear",
  symbol: "BTCUSDT",
});

// Get order history
const history = await client.order.getHistory({
  category: "linear",
  symbol: "BTCUSDT",
  limit: 50,
});

// Cancel all orders
await client.order.cancelAll({
  category: "linear",
  symbol: "BTCUSDT",
});

// Batch place orders (spot/option only)
const batch = await client.order.placeBatch({
  category: "spot",
  request: [
    { category: "spot", symbol: "BTCUSDT", side: "Buy", orderType: "Limit", qty: "0.01", price: "49000" },
    { category: "spot", symbol: "BTCUSDT", side: "Buy", orderType: "Limit", qty: "0.01", price: "48000" },
  ],
});

// Batch amend / cancel
await client.order.amendBatch({ category: "spot", request: [/* ... */] });
await client.order.cancelBatch({ category: "spot", request: [/* ... */] });

Position

// Get position list
const positions = await client.position.getList({
  category: "linear",
  symbol: "BTCUSDT",
});

// Set leverage
await client.position.setLeverage({
  category: "linear",
  symbol: "BTCUSDT",
  buyLeverage: "10",
  sellLeverage: "10",
});

// Switch margin mode (isolated/cross)
await client.position.switchMarginMode({
  category: "linear",
  symbol: "BTCUSDT",
  tradeMode: "ISOLATED",
  buyLeverage: "10",
  sellLeverage: "10",
});

// Set risk limit
await client.position.setRiskLimit({
  category: "linear",
  symbol: "BTCUSDT",
  riskId: 1,
});

// Set take profit / stop loss
await client.position.setTradingStop({
  category: "linear",
  symbol: "BTCUSDT",
  takeProfit: "60000",
  stopLoss: "45000",
  positionIdx: 0,
});

// Switch position mode (one-way / hedge)
await client.position.switchPositionMode({
  category: "linear",
  symbol: "BTCUSDT",
  mode: 0, // 0 = merged single, 3 = both side
});

// Get closed PnL
const pnl = await client.position.getClosedPnl({
  category: "linear",
  symbol: "BTCUSDT",
});

Account

// Get wallet balance
const wallet = await client.account.getWalletBalance({
  accountType: "UNIFIED",
  coin: "USDT",
});

// Get account info
const info = await client.account.getAccountInfo();

// Get fee rate
const fees = await client.account.getFeeRate({
  category: "linear",
  symbol: "BTCUSDT",
});

// Get transaction log
const log = await client.account.getTransactionLog({
  category: "linear",
  limit: 50,
});

// Set margin mode
await client.account.setMarginMode({
  setMarginMode: "REGULAR_MARGIN",
});

Asset

// Get account coins balance
const balance = await client.asset.getAccountCoinsBalance({
  accountType: "UNIFIED",
  coin: "USDT",
});

// Internal transfer between accounts
const transfer = await client.asset.createInternalTransfer({
  transferId: "uuid-here",
  coin: "USDT",
  amount: "100",
  fromAccountType: "UNIFIED",
  toAccountType: "FUND",
});

// Get transfer records
const records = await client.asset.getInternalTransferRecords();

// Get deposit records
const deposits = await client.asset.getDepositRecords({ coin: "USDT" });

// Get withdrawal records
const withdrawals = await client.asset.getWithdrawalRecords({ coin: "USDT" });

// Get coin info (chains, fees, limits)
const coins = await client.asset.getCoinInfo({ coin: "USDT" });

Spot Leveraged Tokens

// Get leveraged token info
const tokens = await client.spotLeverToken.getInfo({ ltCoin: "BTC3L" });

// Get leveraged token market data
const market = await client.spotLeverToken.getMarket({ ltCoin: "BTC3L" });

// Purchase leveraged tokens
const purchase = await client.spotLeverToken.purchase({
  ltCoin: "BTC3L",
  ltAmount: "100",
});

// Redeem leveraged tokens
const redeem = await client.spotLeverToken.redeem({
  ltCoin: "BTC3L",
  ltAmount: "100",
});

// Get purchase/redeem records
const history = await client.spotLeverToken.getRecords({ ltCoin: "BTC3L" });

Spot Margin Trade

// Get margin mode status
const status = await client.spotMarginTrade.getStatus();

// Toggle spot margin mode
await client.spotMarginTrade.toggle({ spotMarginMode: "1" });

// Set spot margin leverage
await client.spotMarginTrade.setLeverage({ leverage: "5" });

// Get VIP margin data
const vipData = await client.spotMarginTrade.getVipMarginData();

// Get borrow orders
const borrows = await client.spotMarginTrade.getBorrowOrders();

WebSocket API Reference

WebSocket Options

// Public WebSocket — requires a category to select the stream
const pub = new BybitPublicWs({
  category: "linear",           // Required: "spot" | "linear" | "inverse" | "option"
  testnet: false,                // Use testnet (default: false)
  wsUrl: "wss://...",            // Custom WebSocket URL (overrides testnet/category)
  pingIntervalMs: 20000,         // Heartbeat interval (default: 20000)
  maxReconnectAttempts: 10,      // Max reconnect attempts, 0 = infinite (default: 10)
  baseReconnectDelayMs: 1000,    // Base delay for reconnect backoff (default: 1000)
  logger: console,               // Custom logger (default: silent)
});

// Private WebSocket — authentication required, no category needed
const priv = new BybitPrivateWs({
  apiKey: "...",                 // Required
  apiSecret: "...",              // Required
  testnet: false,
  authExpirySec: 5,              // Auth signature expiry (default: 5)
  // ...same options as public (except category)
});

Category-based URLs: Each category connects to a different WebSocket endpoint:

| Category | Mainnet URL | Testnet URL | |----------|-------------|-------------| | spot | wss://stream.bybit.com/v5/public/spot | wss://stream-testnet.bybit.com/v5/public/spot | | linear | wss://stream.bybit.com/v5/public/linear | wss://stream-testnet.bybit.com/v5/public/linear | | inverse | wss://stream.bybit.com/v5/public/inverse | wss://stream-testnet.bybit.com/v5/public/inverse | | option | wss://stream.bybit.com/v5/public/option | wss://stream-testnet.bybit.com/v5/public/option | | (private) | wss://stream.bybit.com/v5/private | wss://stream-testnet.bybit.com/v5/private |

To stream multiple categories, create multiple BybitPublicWs instances.

Public Topics

Subscribe using Bybit topic format: "topic.symbol" or "topic.depth.symbol" for orderbook.

| Event | Data Type | Description | |-------|-----------|-------------| | orderbook | WsOrderbook | Orderbook snapshot/delta (depth: 1, 50, 200, 500) | | publicTrade | WsPublicTrade[] | Real-time trades | | tickers | WsTicker | Ticker snapshot updates | | kline | WsKline[] | Candlestick data | | liquidation | WsLiquidation | Liquidation orders |

All public data events receive (type: WsMessageType, data: T, topic: string) where WsMessageType is "snapshot" | "delta".

  • snapshot — Full data snapshot (initial load, periodic refresh)
  • delta — Incremental update (changes only)

The topic string is the full subscription topic (e.g., "orderbook.50.BTCUSDT") so you can identify the symbol, depth, or interval.

ws.subscribe([
  "publicTrade.BTCUSDT",
  "orderbook.50.BTCUSDT",
  "tickers.BTCUSDT",
  "kline.1.BTCUSDT",
  "liquidation.BTCUSDT",
]);

Private Topics

Private topics do not require a symbol — they stream all data for your account.

| Event | Data Type | Description | |-------|-----------|-------------| | order | WsPrivateOrder[] | Order status updates | | execution | WsPrivateExecution[] | Trade executions / fills | | position | WsPrivatePosition[] | Position updates | | wallet | WsPrivateWallet[] | Wallet balance changes | | greeks | WsPrivateGreeks[] | Option greeks updates |

Private events receive (data: T[]) directly — there is no type field for private WebSocket messages.

ws.on("authenticated", () => {
  ws.subscribe(["order", "position", "execution", "wallet"]);
});

Lifecycle Events

Both public and private WebSocket clients emit lifecycle events:

| Event | Args | Description | |-------|------|-------------| | open | — | Connection established (and authenticated for private) | | close | (code, reason) | Connection closed | | reconnecting | (attempt) | Reconnection attempt | | error | (error) | Connection or auth error | | authenticated | — | (Private only) Authentication successful |

ws.on("open", () => console.log("Connected"));
ws.on("close", (code, reason) => console.log(`Disconnected: ${code}`));
ws.on("reconnecting", (attempt) => console.log(`Reconnecting (attempt ${attempt})`));
ws.on("error", (err) => console.error("WS error:", err));

Connection Management

ws.connect();      // Establish connection
ws.disconnect();   // Gracefully disconnect (no auto-reconnect)

ws.subscribe(["publicTrade.BTCUSDT"]);    // Subscribe to topics
ws.unsubscribe(["publicTrade.BTCUSDT"]);  // Unsubscribe from topics

ws.state;               // "connecting" | "connected" | "reconnecting" | "closed"
ws.activeSubscriptions; // string[] of current subscriptions

Utilities

Delta Parser

A generic, zero-dependency delta parser that maintains local state from Bybit WebSocket delta messages (snapshot/delta). Works with any WebSocket data type.

import { DeltaParser } from "@plutarc/bybit";
import type { WsPublicTrade } from "@plutarc/bybit";

// Create a parser for trade data — keys identify unique rows
const tradeParser = new DeltaParser<WsPublicTrade>({
  keys: ["i"],
  maxItems: 1000, // keep only the most recent 1000 trades
});

ws.on("publicTrade", (type, data) => {
  const snapshot = tradeParser.update(type, data);
  console.log(`${snapshot.length} trades in store`);
});

For data where items can be removed (like orderbook levels), use the isRemoved callback:

interface Level {
  price: string;
  size: string;
}

const parser = new DeltaParser<Level>({
  keys: ["price"],
  isRemoved: (item) => item.size === "0", // Bybit convention: size "0" = remove
});

API:

| Method / Property | Returns | Description | |---|---|---| | update(type, data) | readonly T[] | Process a delta message, returns full current state | | snapshot | readonly T[] | Current state without processing a new message | | length | number | Number of items in the store | | clear() | void | Reset the store to empty |

OrderBook Store

A higher-level convenience store for Bybit L2 orderbook data. Uses price-keyed Maps for O(1) updates on Bybit's [price, size] array format. Maintains sorted bids and asks with common orderbook queries.

import { OrderBookStore } from "@plutarc/bybit";

const book = new OrderBookStore();

// Plug directly into a WebSocket event handler
ws.on("orderbook", (type, data) => book.update(type, data));

// Query the book
console.log(book.bestBid());   // 50000
console.log(book.bestAsk());   // 50001
console.log(book.spread());    // 1
console.log(book.midPrice());  // 50000.5

// Get top 5 levels from each side
const { bids, asks } = book.depth(5);

// Volume at top of book
console.log(book.bidVolume(10)); // total bid size in top 10 levels
console.log(book.askVolume());   // total ask size across all levels

// Sorted level arrays
book.bids; // OrderBookLevel[] sorted desc by price (best bid first)
book.asks; // OrderBookLevel[] sorted asc by price (best ask first)

Composability — use as much or as little as you need:

import { DeltaParser, OrderBookStore } from "@plutarc/bybit";

// 1. Batteries included — OrderBookStore handles snapshot/delta directly
const book = new OrderBookStore();
ws.on("orderbook", (type, data) => book.update(type, data));

// 2. DeltaParser only — feed into your own store (zustand, redux, etc.)
ws.on("orderbook", (type, data) => {
  // handle raw { b: [[price, size], ...], a: [[price, size], ...] }
  useMyStore.setState({ orderbook: data });
});

// 3. Neither — handle raw deltas yourself
ws.on("orderbook", (type, data, topic) => { /* your own logic */ });

API:

| Method / Property | Returns | Description | |---|---|---| | update(type, data) | void | Process a snapshot/delta message (auto-bound arrow function) | | bids | readonly OrderBookLevel[] | All bids sorted descending by price | | asks | readonly OrderBookLevel[] | All asks sorted ascending by price | | bestBid() | number \| undefined | Highest bid price | | bestAsk() | number \| undefined | Lowest ask price | | spread() | number \| undefined | Best ask minus best bid | | midPrice() | number \| undefined | Average of best bid and best ask | | depth(n) | { bids, asks } | Top N levels from each side | | bidVolume(n?) | number | Total bid size (optionally top N levels only) | | askVolume(n?) | number | Total ask size (optionally top N levels only) | | clear() | void | Reset the book to empty |

Authentication Helpers

For advanced usage:

import { sign, getSignedHeaders, signWs } from "@plutarc/bybit";

// REST API signature
const sig = sign(apiSecret, timestamp, apiKey, recvWindow, payload);

// Full signed headers (auto-generates timestamp)
const headers = getSignedHeaders(apiKey, apiSecret, recvWindow, payload);

// WebSocket auth signature
const wsSig = signWs(apiSecret, expires);

Error Handling

All REST errors throw typed error classes. Bybit wraps all responses in { retCode, retMsg, result } — the client automatically unwraps successful responses and throws on non-zero retCode, even when the HTTP status is 200.

import { BybitApiError, BybitRateLimitError } from "@plutarc/bybit";

try {
  await client.order.place({ /* ... */ });
} catch (err) {
  if (err instanceof BybitRateLimitError) {
    console.log(`Rate limited — retry after ${err.retryAfterMs}ms`);
  } else if (err instanceof BybitApiError) {
    console.log(`API error ${err.status} (retCode=${err.retCode}): ${err.errorMessage}`);
    console.log(`Retryable: ${err.retryable}`);
  }
}

Retryable errors (automatically retried up to maxRetries times):

  • 429 Too Many Requests
  • 5xx Server errors
  • Network/timeout errors

Non-retryable errors (thrown immediately):

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • Non-zero retCode with HTTP 200 (business logic errors)

Rate Limiting

The client self-throttles to stay within Bybit rate limits:

  • Default: 600 requests per 5-second window (Bybit IP-based limit)
  • Tracks the X-Bapi-Limit-Status response header from Bybit
  • Pauses requests when approaching the limit (5-request buffer)
  • Configurable via rateLimitPer5Seconds option

Note: Bybit also enforces per-UID per-second limits on specific endpoints (e.g., 20 req/s for order creation). These are endpoint-specific and not tracked by the client-side rate limiter.

Testnet

All clients support testnet with a single flag:

// REST
const client = new BybitClient({ testnet: true });
// → https://api-testnet.bybit.com

// Public WebSocket
const ws = new BybitPublicWs({ category: "linear", testnet: true });
// → wss://stream-testnet.bybit.com/v5/public/linear

// Private WebSocket
const priv = new BybitPrivateWs({ apiKey: "...", apiSecret: "...", testnet: true });
// → wss://stream-testnet.bybit.com/v5/private

Get testnet API keys at testnet.bybit.com.

Custom Logger

Pass any object implementing { debug, info, warn, error } methods. By default the client is silent.

// Use console
const client = new BybitClient({ logger: console });

// Use pino
import pino from "pino";
const client = new BybitClient({ logger: pino({ name: "bybit" }) });

// Use winston
import winston from "winston";
const client = new BybitClient({ logger: winston.createLogger({ /* ... */ }) });

Development

# Install dependencies
bun install

# Type check
bun run typecheck

# Run tests
bun test

# Build
bun run build

# Lint & format
bun run lint
bun run format

Resources

License

MIT — see LICENSE for details.