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

@ostium/builder-sdk

v0.3.1

Published

Ostium builder SDK

Readme

Ostium Builder SDK

Ostium Builder Service Banner

TypeScript SDK for trading Stocks, Commodities, Forex, Indices, Crypto and ETF perps on Ostium.

npm install @ostium/builder-sdk viem
# or
bun add @ostium/builder-sdk viem

Guides:

  • BUILDER.md — Integrating Ostium into your app with builder fees
  • TRADER.md — Building trading strategies programmatically on Ostium

Project Structure

src/
  client.ts          # OstiumClient — public API
  config.ts          # Named param interfaces for each mode
  types.ts           # OpenTradeParams, CloseTradeParams, etc.
  errors.ts          # OstiumError + OstiumErrorCode
  encoder.ts         # ABI encoding helpers (openTrade, closeTrade, ...)
  cli.ts             # Interactive CLI (bunx ostium)
  signer/            # SignerStrategy — Self vs Delegated wrapping
  submitter/         # SubmissionStrategy — EOA vs Pimlico UserOp
  internal/
    contracts.ts     # On-chain addresses (mainnet + testnet)
    decimal.ts       # parseUsdc, parsePrice, parseLeverage
    erc20.ts         # Minimal ERC-20 ABI
    contract.ts      # Ostium Trading ABI
    open-builder.ts  # Trade struct construction
    precision.ts     # Precision constants
    validation.ts    # Input validation helpers
  data/          # Read-only subgraph + price feed client
    client.ts        # OstiumSubgraphClient
    types.ts         # Pair, Position, Fill, OpenOrder, Order, …
    queries.ts       # GraphQL queries
    internal/        # Formatters, calculations, pagination
dist/                # Compiled output (bun run build)

Development

bun install          # install dependencies
bun run build        # compile to dist/
bun run typecheck    # tsc --noEmit

Modes

The four client modes differ in who signs and how the transaction is submitted.

Signing

| Mode | Who owns USDC + positions | Who signs transactions | |:--|:--|:--| | Self | Your EOA | Your EOA | | Delegated | Trader address (separate EOA) | Delegate EOA, wrapped in delegatedAction(traderAddress, data) |

Submission

| Mode | How the tx is sent | Gas | |:--|:--|:--| | Self (EOA) | Standard eth_sendRawTransaction | ETH required per tx | | Gasless | Pimlico ERC-4337 UserOperation via Safe smart account | ETH-free after one-time setup |

The Four Combinations

| Constructor | Signer | Submitter | Notes | |:--|:--|:--|:--| | createSelfAndSelf | EOA | EOA | Simplest — one key, pays gas | | createSelfAndGasless | EOA (owner) + Safe (delegate) | Pimlico UserOp | One-time setup, then free | | createDelegatedAndSelf | Delegate EOA | Delegate EOA | Delegate holds ETH, trader holds USDC | | createDelegatedAndGasless | Delegate EOA (owner) + Safe (delegate) | Pimlico UserOp | No ETH after trader calls setDelegate(safeAddress) |

import { OstiumClient, OrderType } from '@ostium/builder-sdk';

// Self + Self
const client = await OstiumClient.createSelfAndSelf({
  traderPrivateKey: '0x...',
  rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/...',
});

// Self + Gasless
const client = await OstiumClient.createSelfAndGasless({
  traderPrivateKey: '0x...',
  pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
});

// Delegated + Self
const client = await OstiumClient.createDelegatedAndSelf({
  delegatePrivateKey: '0xDelegateKey',
  traderAddress: '0xTraderAddress',
  rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/...',
});

// Delegated + Gasless
const client = await OstiumClient.createDelegatedAndGasless({
  delegatePrivateKey: '0xDelegateKey',
  traderAddress: '0xTraderAddress',
  pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
});

Build Unsigned Transactions

Every write method now has a get*Tx() counterpart that returns transaction data without signing or submitting it.

const client = await OstiumClient.createSelfAndSelf({
  traderAddress: '0xTraderAddress',
});

const tx = client.getOpenTradeTx({
  pairId: 0,
  buy: true,
  price: '65000',
  collateral: '100',
  leverage: '5',
  type: OrderType.Market,
});

if (tx.kind === 'eoa') {
  await window.ethereum.request({
    method: 'eth_sendTransaction',
    params: [{
      from: tx.from,
      to: tx.to,
      data: tx.data,
      value: `0x${tx.value.toString(16)}`,
    }],
  });
}

Gasless build-only clients return a Safe-style request instead:

const gaslessClient = await OstiumClient.createSelfAndGasless({
  traderAddress: '0xTraderAddress',
  safeAddress: '0xSafeAddress',
});

const safeTx = gaslessClient.getOpenTradeTx({ ... });
// safeTx.kind === 'safe'
// safeTx.safeAddress identifies the Safe that should execute safeTx.calls[0]

The existing write methods (openTrade, closeTrade, approveUsdc, etc.) still work the same way, but they now submit the transaction built by the corresponding get*Tx() method.

Read-only (no private key)

Use createReadOnly to access market data without a signer. All read methods are available; write methods throw INVALID_CONFIG.

const reader = await OstiumClient.createReadOnly();
const { pairs } = await reader.getPairs();
const positions = await reader.getOpenPositions({ user: '0xTrader...' });
const balances = await reader.getBalances('0xTrader...');

Stream Position Updates

If you already have an OpenPositionsResponse, you can stream live price-driven updates for those positions without re-fetching the full position set.

const positions = await client.getOpenPositions();
const stream = client.streamPositionUpdates(positions);

stream.onUpdate(next => {
  console.log(next.marginSummary.totalRawPnlUsd);
  console.log(next.pairPositions[0]?.position.unrealizedPnl);
});

stream.onError(err => console.error(err));
stream.close();

The SDK subscribes once per unique pairId in the response, then updates the full OpenPositionsResponse as prices change.

Common Options

All constructors (including createReadOnly) accept these optional overrides:

{
  testnet?:       boolean;  // Arbitrum Sepolia when true (default false)
  subgraphUrl?:   string;   // override the default subgraph endpoint
  builderApiUrl?: string;   // override the builder API base URL (prices, OHLC, WebSocket)
}

Example:

const client = await OstiumClient.createSelfAndSelf({
  traderPrivateKey: '0x...',
  rpcUrl: '...',
  subgraphUrl:   'https://your-subgraph.example.com/graphql',
  builderApiUrl: 'https://your-builder-api.example.com',
});

const reader = await OstiumClient.createReadOnly({
  subgraphUrl:   'https://your-subgraph.example.com/graphql',
  builderApiUrl: 'https://your-builder-api.example.com',
});

Self + Gasless: One-Time Setup

A Safe smart account is derived deterministically from your private key. You need to register it as your on-chain delegate once:

const client = await OstiumClient.createSelfAndGasless({
  traderPrivateKey: '0x...',
  pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
});

console.log('EOA (trader):', client.getTraderAddress());
console.log('Safe (delegate):', client.getSmartAccountAddress());

await client.approveUsdc('max');         // EOA → TradingStorage approval (gas, once)
await client.setupGaslessDelegation();   // EOA → setDelegate(Safe) (gas, once)

// All trades from here are gasless UserOps
await client.openTrade({ ... });

Delegated Modes: Setup Checklist

The trader account must perform two one-time operations before the delegate can trade:

// 1. Trader approves USDC
const traderClient = await OstiumClient.createSelfAndSelf({
  traderPrivateKey: '0xTraderKey',
  rpcUrl: '...',
});
await traderClient.approveUsdc('max');

// 2a. Delegated + Self: trader registers the delegate EOA
await traderClient.setDelegate('0xDelegateEOA');

// 2b. Delegated + Gasless: trader registers the delegate's Safe
const delegateClient = await OstiumClient.createDelegatedAndGasless({
  delegatePrivateKey: '0xDelegateKey',
  traderAddress: '0xTraderAddress',
  pimlicoUrl: '...',
});
await traderClient.setDelegate(delegateClient.getSmartAccountAddress()!);

CLI — Environment Variable Configuration

The ostium CLI can load all credentials from a .env file (via dotenv). Create a .env file in your working directory with the variables relevant to your mode:

# ── Universal ─────────────────────────────────────────────────────────────────

# Self / main EOA private key (required for Self modes; also used as the main key in Test + Delegated)
PRIVATE_KEY=0x...

# Arbitrum RPC URL — required for Self+Self and Delegated+Self; optional for gasless modes
ARB_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY

# ── Delegated modes ───────────────────────────────────────────────────────────

# Delegate EOA private key (falls back to PRIVATE_KEY if not set)
DELEGATE_PRIVATE_KEY=0x...

# Trader address the delegate acts on behalf of (required for Trade + Delegated)
TRADER_ADDRESS=0x...

# ── Gasless modes ─────────────────────────────────────────────────────────────

# Pimlico sponsor URL (required for Self+Gasless and Delegated+Gasless)
PIMLICO_URL=https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161

When the CLI detects a .env file it skips the interactive key/URL prompts and loads all values automatically. Fields not present in the env file are prompted interactively as normal.


Networks

| Network | Chain ID | testnet | |:--|:-:|:-:| | Arbitrum One | 42161 | false (default) | | Arbitrum Sepolia | 421614 | true |

const client = await OstiumClient.createSelfAndSelf({
  traderPrivateKey: '0x...',
  rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc',
  testnet: true,
});

API Reference

Enums

import { OrderType, CancelOrderType } from '@ostium/builder-sdk';

OrderType.Market  // 'market'
OrderType.Limit   // 'limit'
OrderType.Stop    // 'stop'

CancelOrderType.Limit        // 'limit'
CancelOrderType.PendingOpen  // 'pendingOpen'
CancelOrderType.PendingClose // 'pendingClose'

Trading Methods

All return Promise<SubmissionResult> as soon as the transaction is submitted (UserOp or EOA). The hash is available immediately; the SDK does not wait for receipt or parse oracle events.

interface SubmissionResult {
  txHash: `0x${string}`;
  smartAccountAddress?: `0x${string}`; // gasless modes only
}

Use getOrders({ initiatedTxHashes: [result.txHash] }) to poll by submission hash, or orderIds / recent orders for the trader.

openTrade(params)

const result = await client.openTrade({
  pairId:      0,                  // trading pair (use getPairs() to list all)
  buy:         true,               // true = long, false = short
  price:       '65000',            // entry price
  collateral:  '100',              // USD (min $5)
  leverage:    '10',               // multiplier (max varies by pair)
  type:        OrderType.Market,
  takeProfit?: '70000',
  stopLoss?:   '60000',
  slippage?:   50,                 // bps, market orders only
  isDayTrade?: false,              // true when leverage > pair.overnightMaxLeverage
});
console.log(result.txHash);

Day trades are auto-closed before market close. Set isDayTrade: true when the desired leverage exceeds pair.overnightMaxLeverage (and overnightMaxLeverage > 0). Use getPairs() to read both thresholds per pair.

closeTrade(params)

pairId and idx come from getOpenPositions():

const { pairPositions } = await client.getOpenPositions();
const { pairId, idx } = pairPositions[0].position;

await client.closeTrade({
  pairId,           // from Position.pairId
  idx,              // from Position.idx
  price: '66000',
  closePercent: 100,  // 1–100, partial closes supported
  slippage?: 50,
});

cancelOrder(params)

TypeScript narrows the required fields by type:

// Cancel a pending limit order
const [order] = await client.getOpenOrders();
await client.cancelOrder({ type: CancelOrderType.Limit, pairId: order.pairId, idx: order.idx });

// Cancel a timed-out market open (orderId from chain / indexer)
await client.cancelOrder({ type: CancelOrderType.PendingOpen, orderId: 123 });

// Cancel a timed-out market close — retry: true re-submits the close
await client.cancelOrder({ type: CancelOrderType.PendingClose, orderId: 123, retry: true });

modifyOrder(params)

pairId and idx come from getOpenPositions() (for TP/SL) or getOpenOrders() (for limit order price):

// Update TP on an open trade
await client.modifyOrder({ pairId, idx, takeProfit: '72000' });

// Update SL on an open trade
await client.modifyOrder({ pairId, idx, stopLoss: '61000' });

// Reprice a limit order (with optional new TP/SL)
await client.modifyOrder({ pairId, idx, price: '64000', takeProfit: '70000' });

Note: you cannot update both takeProfit and stopLoss in a single call without also providing price. Send two separate calls instead.

updateCollateral(params)

pairId and idx come from getOpenPositions():

await client.updateCollateral({ pairId, idx, amount: '50' });   // add $50
await client.updateCollateral({ pairId, idx, amount: '-30' });  // remove $30

USDC + Balance Helpers

const { current, required, sufficient } = await client.checkUsdcAllowance('100');
await client.approveUsdc('1000');   // approve $1000
await client.approveUsdc('max');    // approve MaxUint256

// All three values are decimal strings produced by formatUnits (e.g. "1234.56")
// Parse with Number() / parseFloat() only when you need numeric arithmetic.
const { usdc, eth, allowance } = await client.getBalances();
// In read-only mode, pass the address explicitly:
const { usdc, eth, allowance } = await client.getBalances('0xTrader...');

Read Methods

All read methods are available on both OstiumClient and OstiumSubgraphClient. On OstiumClient the connected trader address is used by default wherever user is optional.

getPairs(params?)

All trading pairs with live prices, market-status flags, computed size limits, and rollover rates.

const { pairs } = await client.getPairs();
// Optional — filter to specific pairs:
const { pairs } = await client.getPairs({ pairIds: [0, 1, 4] });

// Pair fields:
// pairId, pairTo, pairFrom, category
// maxLeverage, overnightMaxLeverage
// minSz, maxBSz, maxSSz, minNtl
// openInterest, buyOpenInterest, sellOpenInterest, maxOpenInterest
// rolloverRate: { long, short }   — 8hr % by side
// rolloverFeePerBlock
// midPx, askPx, bidPx
// isMarketOpen, isDayTradingClosed, secondsToToggleIsDayTradingClosed

Pair symbol names from the subgraph are normalized automatically (e.g. CL → WTI, FTSE → UK100, SPX → US500).

getAllPrices()

Live mid/bid/ask prices for every pair, keyed by pairId.

const { prices } = await client.getAllPrices();
// prices['0'] → { mid: '65000', bid: '64990', ask: '65010' }

getOpenPositions(params?)

Open positions, margin summary, and per-position live PnL. Block number is fetched automatically on OstiumClient.

const { pairPositions, marginSummary, time } =
  await client.getOpenPositions();

// Each position:
const { pairId, pid, idx, side, szi, entryPx, leverage, ntl,
        unrealizedPnl, returnOnEquity, liquidationPx,
        collateralUsed, cumRollover, tpPx, slPx,
        openTimestamp, isDayTrade } = pairPositions[0].position;

// marginSummary: accountValue, totalCollateralUsed, totalNtlPos, totalRawPnlUsd
// marginSummary.totalWithdrawable: max collateral removable across all positions
// time: server time (Unix ms)

getFills(params?)

Executed fills, newest first.

const fills = await client.getFills();
// Filter options:
const fills = await client.getFills({ pairId: 0, limit: 50 });
// All traders (no address filter):
const fills = await client.getFills({ user: 'ALL' });

getFillsByTime(params)

Same as getFills but with a time range filter (Unix milliseconds).

const fills = await client.getFillsByTime({
  startTime: Date.now() - 7 * 86_400_000,  // last 7 days
  endTime:   Date.now(),                    // optional, defaults to now
  pairId:    0,                             // optional pair filter
});

getOpenOrders(params?)

Active limit orders for a trader.

const orders = await client.getOpenOrders();
// Each order: pairId, pairTo, pairFrom, idx, side, limitPx, szi, orderType, tpPx?, slPx?, timestamp

getOrders(params?)

Orders at any status — pending, executed, or cancelled. Use this to poll whether an openTrade/closeTrade was executed by the oracle.

const result = await client.openTrade({ /* … */ });

// Poll by the submission tx (matches subgraph `initiatedTx`):
const [order] = await client.getOrders({ initiatedTxHashes: [result.txHash] });
console.log(order.isPending, order.isCancelled);

// Or by on-chain order id:
const [byId] = await client.getOrders({ orderIds: [123] });

// Recent orders for the connected trader (no filter args):
const orders = await client.getOrders();

Each Order extends Fill with initiatedTx, initiatedTime, isPending, isCancelled, and optional cancelReason.

getSimSlippage(params)

Simulate price impact for a list of pairs and notionals.

const result = await client.getSimSlippage({
  pairIds: [0, 1],
  ntls: ['1000', '10000', '100000'],
});
// result['0'].long  → [{ ntl, slippage }, ...]
// result['0'].short → [{ ntl, slippage }, ...]

getSimOrderbook(params)

Synthetic bid/ask orderbook for a pair, following Hyperliquid L2Book format. Levels are log-spaced from the minimum order size to the remaining OI capacity on each side.

const book = await client.getSimOrderbook({ pairId: 0, levels: 20 });
// book.levels[0] → bids (short entries), best bid first
// book.levels[1] → asks (long entries), best ask first
// Each level: { px: string, sz: string, n: 1 }

getCandles(params)

OHLC candles from the builder API. from/to are Unix milliseconds. pairId is the same value returned by getPairs().

const candles = await client.getCandles({
  pairId: 0,
  from: Date.now() - 30 * 86_400_000,  // last 30 days
  to: Date.now(),                       // optional, defaults to now
  resolution: '1D',                     // '1' | '5' | '15' | '60' | '240' | '1D'
});
// Each candle: { pairFrom, pairTo, time, open, high, low, close }
// pairFrom / pairTo use normalized display names (e.g. "WTI" not "CL")

streamPrices(pairIds?)

WebSocket connection to the live price feed. Fires a snapshot on connect, then a tick on every update. Accepts the same pairId values as everywhere else in the SDK — no pair name strings needed.

const stream = client.streamPrices([0, 1]);  // BTC and ETH by pairId

stream.onOpen(() => console.log('connected'));
stream.onSnapshot(ticks => console.log('initial snapshot:', ticks.length));
stream.onTick(tick => console.log(tick.pair, tick.mid));
// tick.pair / tick.from / tick.to use normalized display names

// Dynamically add / remove by pairId:
stream.subscribe([2]);
stream.unsubscribe([0]);

// Clean up:
stream.close();

Requires Node.js 18+, Bun, or a browser environment.

Getters

client.getTraderAddress();       // on-chain trader address (throws in read-only mode)
client.getSmartAccountAddress(); // Safe address — gasless modes only, else undefined
client.isReadOnly();             // true when created via createReadOnly()

Error Codes

import { OstiumError, OstiumErrorCode } from '@ostium/builder-sdk';

| Code | When thrown | |:--|:--| | INVALID_CONFIG | Missing or malformed config (privateKey format, address format, fee range) | | VALIDATION_FAILED | Invalid trade params (TP/SL direction, leverage range, close percent) | | ALLOWANCE_INSUFFICIENT | USDC allowance too low for openTrade / updateCollateral | | DELEGATION_FAILED | approveUsdc() called in delegated mode | | CONTRACT_ERROR | Contract revert (e.g. WrongLeverage, NoDelegate) | | NETWORK_ERROR | RPC / Pimlico connectivity issues or rate limits | | SUBMISSION_FAILED | Transaction rejected for other reasons |

Decimal Utilities

import { parseUsdc, parsePrice, parseLeverage, MIN_COLLATERAL_USD, MAX_COLLATERAL_USD } from '@ostium/builder-sdk';

parsePrice('65000.50');   // 65000500000000000000000n (18 decimals)
parseUsdc('100.50');      // 100500000n (6 decimals)
MIN_COLLATERAL_USD;       // 5
MAX_COLLATERAL_USD;       // 2_000_000

Standalone Subgraph Client

For read-only market data without a trading key, use OstiumSubgraphClient directly:

import { OstiumSubgraphClient } from '@ostium/builder-sdk';

const subgraph = await OstiumSubgraphClient.create({ testnet: false });

const { pairs }    = await subgraph.getPairs();
const { prices }   = await subgraph.getAllPrices();
const positions    = await subgraph.getOpenPositions({ user: '0xTrader...' });
const fills        = await subgraph.getFills({ user: '0xTrader...', limit: 50 });

OstiumSubgraphClient.getOpenPositions accepts an optional blockNumber (required for accurate PnL). On OstiumClient this is fetched automatically.