@ostium/builder-sdk
v0.3.1
Published
Ostium builder SDK
Maintainers
Keywords
Readme
Ostium Builder SDK

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 viemGuides:
- 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 --noEmitModes
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=42161When 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: truewhen the desired leverage exceedspair.overnightMaxLeverage(andovernightMaxLeverage > 0). UsegetPairs()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 $30USDC + 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, secondsToToggleIsDayTradingClosedPair 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?, timestampgetOrders(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_000Standalone 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.
