# @ostium/builder-sdk — flat reference for AI coding agents Build a perps trading integration on Ostium (Arbitrum). Everything an agent needs is in this one file; no cross-referencing required. Ostium is a perpetuals exchange on Arbitrum. This SDK gives you reads (pairs, prices, positions, history), pre-trade previews, and trade submission across four signing/submission modes. Everything below is prescriptive: follow it directly rather than cross-referencing other docs. `README.md`, `BUILDER.md` and `TRADER.md` in this repo are the long-form human docs. ## Install ```bash npm install @ostium/builder-sdk viem ``` `viem` is a peer dependency. Node 18+, Bun, or a browser. ## Pick a mode first This is the only decision that changes your code shape. Pick by answering *who holds the key* and *who pays gas*: | Your users… | Use | Gas | |---|---|---| | Hold their own keys, sign in their wallet | `createReadOnly` + `get*Tx()` builders | User | | Gave you a key you hold server-side | `createSelfAndSelf` | Your key | | Want no ETH at all | `createSelfAndGasless` | Sponsored (Pimlico) | | Delegated trading to your backend | `createDelegatedAndSelf` | Your delegate | | Delegated, and you sponsor gas | `createDelegatedAndGasless` | Sponsored | | Market data only | `createReadOnly` | — | ```ts import { OstiumClient } from '@ostium/builder-sdk'; // Server-side, you hold the key: const client = await OstiumClient.createSelfAndSelf({ traderPrivateKey: process.env.PRIVATE_KEY, rpcUrl: process.env.ARBITRUM_RPC_URL, builder: { address: '0xYourBuilderAddress', feeBps: 10 }, // your revenue share }); // Frontend / market data — no key. createReadOnly takes no trader address; // pass the user per call instead. const reader = await OstiumClient.createReadOnly(); await reader.getOpenPositions({ user: userAddress }); ``` Add `testnet: true` for Arbitrum Sepolia. ## Builder fees `builder: { address, feeBps }` on any create* call attributes every trade to you and pays you `feeBps` of notional. Set it once at construction; it applies automatically to `openTrade`. Read your earnings with `client.getBuilderOrders(yourAddress)`. ## Recipe: order ticket Always call `previewOpenTrade` before `openTrade`. It returns execution price after spread, the fee breakdown, the resulting liquidation price, and the pre-trade validation checks it can make off-chain. It uses `@ostium/formulae` — the same math the contracts run — so your numbers match ostium.io. ```ts const preview = await client.previewOpenTrade({ pairId: 0, // BTC/USD — see getPairs() isLong: true, collateral: 1000, // USDC, gross (fees come out of this) leverage: 10, // limitPrice: 95_000, // omit for a market order // isDayTrade: true, // unlocks the pair's higher intraday leverage cap }); preview.entryPx; // execution price after dynamic spread preview.fees.total; // deducted from collateral at open preview.collateralAtOpen; // what actually backs the position preview.positionSize; // in base-asset units preview.liquidationPx; preview.effectiveMaxLeverage; preview.isValid; // false when `warnings` is non-empty if (!preview.isValid) { // codes: LEVERAGE_ABOVE_MAX | LEVERAGE_BELOW_MIN | BELOW_MIN_POSITION_SIZE // FEES_EXCEED_COLLATERAL | ABOVE_EXPOSURE_LIMIT | MARKET_CLOSED return preview.warnings.map(w => w.message); } ``` Then submit. Note the field names differ from the preview — submission uses `buy`, `type`, and decimal **strings**: ```ts import { OrderType } from '@ostium/builder-sdk'; await client.openTrade({ pairId: 0, buy: true, // NOT `isLong` collateral: '1000', // decimal strings, not numbers leverage: '10', price: preview.entryPx, // always required, including for market orders type: OrderType.Market, // .Market | .Limit | .Stop // takeProfit: '110000', stopLoss: '90000', slippage: 100 (bps), isDayTrade: true // builder: { address, feeBps } // per-trade override of the client default }); ``` Preview takes **numbers** and `isLong`; submission takes **decimal strings** and `buy`. This trips people up — do not assume the shapes match. ## Recipe: onboarding a new trader Never hand-roll the approve/delegate sequence — ask what is outstanding: ```ts const status = await client.getOnboardingStatus({ requiredUsd: '1000' }); if (!status.ready) { for (const step of status.steps) { console.log(step.description); // render as a checklist await wallet.sendTransaction(step.tx); // trader signs from their OWN account } } ``` A delegate can neither approve USDC nor register itself — every onboarding step must come from the trader's own account. ## Recipe: positions and history ```ts // NOTE the field name: `pairPositions`, not `positions`. Each entry wraps the // position: `pairPositions[0].position`. const { pairPositions, marginSummary } = await client.getOpenPositions({ user: trader }); for (const { position } of pairPositions) { // position carries unrealizedPnl, liquidationPx, returnOnEquity, // maxWithdrawable, cumRollover — already computed. Do not recompute these. } await client.getFills({ user: trader, limit: 50 }); // executed trades await client.getOpenOrders({ user: trader }); // resting limit/stop orders await client.getOrders({ user: trader }); // order history, incl. pending await client.getPairs(); // all markets + live prices await client.getAllPrices(); await client.getCandles({ pairId: 0, from: Date.now() - 7 * 86_400_000, resolution: '1D' }); ``` ## Recipe: live data (WebSocket) ```ts // Prices const prices = client.streamPrices([0, 1]); prices.onTick(t => console.log(t.pair, t.mid)); prices.close(); // Positions + orders + balances for a trader, with optimistic overlays. // Use this to drive a live UI — it reflects a submitted trade before it indexes. const updates = client.streamAccountUpdates({ user: [traderAddress] }) // `user` takes an ARRAY; updates.onUpdate(snapshot => render(snapshot)); updates.onError(err => console.error(err)); updates.close(); ``` `Position.confirmationStatus` moves `optimistic → initiated → executed → indexed`. Treat `idx` as unreliable until `executed`. ## Recipe: standalone math (bots, backtests) Pure functions, plain numbers, no client and no network: ```ts import { liquidationPrice, pnl } from '@ostium/builder-sdk'; liquidationPrice({ entryPx: 100_000, isLong: true, collateral: 1000, leverage: 10, maxLeverage: 100 }); pnl({ entryPx: 100_000, markPx: 105_000, isLong: true, collateral: 1000, leverage: 10 }); // → { netPnl: 500, netPnlPercent: 50, netValue: 1500 } ``` ## Managing positions ```ts import { CancelOrderType } from '@ostium/builder-sdk'; // Close 50% at the current market price. await client.closeTrade({ pairId: 0, idx: 0, price: '95000', closePercent: 50 }); // Set or change TP/SL on an open position (or a limit order's trigger price). await client.modifyOrder({ pairId: 0, idx: 0, takeProfit: '110000', stopLoss: '90000' }); // Signed amount: positive adds collateral, negative removes it. await client.updateCollateral({ pairId: 0, idx: 0, amount: '500' }); await client.updateCollateral({ pairId: 0, idx: 0, amount: '-200' }); // cancelOrder is a discriminated union on `type`: await client.cancelOrder({ type: CancelOrderType.Limit, pairId: 0, idx: 0 }); await client.cancelOrder({ type: CancelOrderType.PendingOpen, orderId: 123 }); await client.cancelOrder({ type: CancelOrderType.PendingClose, orderId: 123, retry: true }); ``` Positions are addressed by `(pairId, idx)`, not by a global id. Get `idx` from `getOpenPositions()` — and only trust it once `confirmationStatus` reaches `"executed"`. ## Errors ```ts import { OstiumError, OstiumErrorCode } from '@ostium/builder-sdk'; try { await client.openTrade({ ... }); } catch (e) { if (e instanceof OstiumError && e.code === OstiumErrorCode.ALLOWANCE_INSUFFICIENT) { await client.approveUsdc('max'); } } ``` Codes: `INVALID_CONFIG`, `VALIDATION_FAILED`, `ALLOWANCE_INSUFFICIENT`, `SUBMISSION_FAILED`, `DELEGATION_FAILED`, `CONTRACT_ERROR`, `NETWORK_ERROR`. Reads throw `OstiumSubgraphError` with `OstiumSubgraphErrorCode` instead. ## Rules that save you a debugging session 1. **Preview before submitting.** `previewOpenTrade` reports the pre-trade validation failures it knows about, off-chain and for free. It is not a guarantee: chain state moves between preview and submission, and not every revert has a warning. Keep normal error handling around `openTrade`. 2. **Numbers for preview and math, strings for submission.** 3. **Don't recompute PnL or liquidation** for open positions — `Position` already carries them, computed from the same formulae the contracts use. 4. **Don't add trading math to your integration.** If a number is missing, it belongs in `@ostium/formulae`, not in your app. 5. **`pairId` is a number/numeric string**, not a ticker. Resolve tickers via `getPairs()`. 6. **Approve USDC once**, then check `getOnboardingStatus()` rather than assuming. 7. **Every value comes back already scaled** — `leverage: 10` is 10x, `collateral: '49.2'` is USDC. Never divide by 1e18. ## Full export surface ``` OstiumClient OstiumSubgraphClient OrderType CancelOrderType OstiumError OstiumErrorCode OstiumSubgraphError OstiumSubgraphErrorCode OstiumPriceStream OstiumPositionUpdatesStream OstiumAccountUpdatesStream liquidationPrice pnl parseUsdc parsePrice parseLeverage extractOrderIdFromReceipt PRECISION_6 PRECISION_18 DEFAULT_SLIPPAGE_PERCENTAGE MIN_OPEN_SIZE_USD MIN_COLLATERAL_USD MAX_COLLATERAL_USD DEFAULT_SUBGRAPH_ENDPOINT DEFAULT_SUBGRAPH_ENDPOINT_TESTNET DEFAULT_BUILDER_API_URL ``` Runnable end-to-end programs live in `examples/`.