@ostium/builder-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,589 @@
1
+ # Ostium Builder SDK
2
+
3
+ ![Ostium Builder Service Banner](./builder-service.png)
4
+
5
+ TypeScript SDK for trading Stocks, Commodities, Forex, Indices, Crypto and ETF perps on [Ostium](https://ostium.com).
6
+
7
+ ```bash
8
+ npm install @ostium/builder-sdk viem
9
+ # or
10
+ bun add @ostium/builder-sdk viem
11
+ ```
12
+
13
+ **Guides:**
14
+ - [BUILDER.md](BUILDER.md) — Integrating Ostium into your app with builder fees
15
+ - [TRADER.md](TRADER.md) — Building trading strategies programmatically on Ostium
16
+
17
+ ---
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ src/
23
+ client.ts # OstiumClient — public API
24
+ config.ts # Named param interfaces for each mode
25
+ types.ts # OpenTradeParams, CloseTradeParams, etc.
26
+ errors.ts # OstiumError + OstiumErrorCode
27
+ encoder.ts # ABI encoding helpers (openTrade, closeTrade, ...)
28
+ cli.ts # Interactive CLI (bunx ostium)
29
+ signer/ # SignerStrategy — Self vs Delegated wrapping
30
+ submitter/ # SubmissionStrategy — EOA vs Pimlico UserOp
31
+ internal/
32
+ contracts.ts # On-chain addresses (mainnet + testnet)
33
+ decimal.ts # parseUsdc, parsePrice, parseLeverage
34
+ erc20.ts # Minimal ERC-20 ABI
35
+ contract.ts # Ostium Trading ABI
36
+ open-builder.ts # Trade struct construction
37
+ precision.ts # Precision constants
38
+ validation.ts # Input validation helpers
39
+ data/ # Read-only subgraph + price feed client
40
+ client.ts # OstiumSubgraphClient
41
+ types.ts # Pair, Position, Fill, OpenOrder, Order, …
42
+ queries.ts # GraphQL queries
43
+ internal/ # Formatters, calculations, pagination
44
+ dist/ # Compiled output (bun run build)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ bun install # install dependencies
53
+ bun run build # compile to dist/
54
+ bun run typecheck # tsc --noEmit
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Modes
60
+
61
+ The four client modes differ in **who signs** and **how the transaction is submitted**.
62
+
63
+ ### Signing
64
+
65
+ | Mode | Who owns USDC + positions | Who signs transactions |
66
+ |:--|:--|:--|
67
+ | **Self** | Your EOA | Your EOA |
68
+ | **Delegated** | Trader address (separate EOA) | Delegate EOA, wrapped in `delegatedAction(traderAddress, data)` |
69
+
70
+ ### Submission
71
+
72
+ | Mode | How the tx is sent | Gas |
73
+ |:--|:--|:--|
74
+ | **Self** (EOA) | Standard `eth_sendRawTransaction` | ETH required per tx |
75
+ | **Gasless** | Pimlico ERC-4337 UserOperation via Safe smart account | ETH-free after one-time setup |
76
+
77
+ ### The Four Combinations
78
+
79
+ | Constructor | Signer | Submitter | Notes |
80
+ |:--|:--|:--|:--|
81
+ | `createSelfAndSelf` | EOA | EOA | Simplest — one key, pays gas |
82
+ | `createSelfAndGasless` | EOA (owner) + Safe (delegate) | Pimlico UserOp | One-time setup, then free |
83
+ | `createDelegatedAndSelf` | Delegate EOA | Delegate EOA | Delegate holds ETH, trader holds USDC |
84
+ | `createDelegatedAndGasless` | Delegate EOA (owner) + Safe (delegate) | Pimlico UserOp | No ETH after trader calls `setDelegate(safeAddress)` |
85
+
86
+ ```ts
87
+ import { OstiumClient, OrderType } from '@ostium/builder-sdk';
88
+
89
+ // Self + Self
90
+ const client = await OstiumClient.createSelfAndSelf({
91
+ traderPrivateKey: '0x...',
92
+ rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/...',
93
+ });
94
+
95
+ // Self + Gasless
96
+ const client = await OstiumClient.createSelfAndGasless({
97
+ traderPrivateKey: '0x...',
98
+ pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
99
+ });
100
+
101
+ // Delegated + Self
102
+ const client = await OstiumClient.createDelegatedAndSelf({
103
+ delegatePrivateKey: '0xDelegateKey',
104
+ traderAddress: '0xTraderAddress',
105
+ rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/...',
106
+ });
107
+
108
+ // Delegated + Gasless
109
+ const client = await OstiumClient.createDelegatedAndGasless({
110
+ delegatePrivateKey: '0xDelegateKey',
111
+ traderAddress: '0xTraderAddress',
112
+ pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
113
+ });
114
+ ```
115
+
116
+ ### Read-only (no private key)
117
+
118
+ Use `createReadOnly` to access market data without a signer. All read methods are available; write methods throw `INVALID_CONFIG`.
119
+
120
+ ```ts
121
+ const reader = await OstiumClient.createReadOnly();
122
+ const { pairs } = await reader.getPairs();
123
+ const positions = await reader.getOpenPositions({ user: '0xTrader...' });
124
+ const balances = await reader.getBalances('0xTrader...');
125
+ ```
126
+
127
+ ### Common Options
128
+
129
+ All constructors (including `createReadOnly`) accept these optional overrides:
130
+
131
+ ```ts
132
+ {
133
+ testnet?: boolean; // Arbitrum Sepolia when true (default false)
134
+ subgraphUrl?: string; // override the default subgraph endpoint
135
+ builderApiUrl?: string; // override the builder API base URL (prices, OHLC, WebSocket)
136
+ }
137
+ ```
138
+
139
+ Example:
140
+
141
+ ```ts
142
+ const client = await OstiumClient.createSelfAndSelf({
143
+ traderPrivateKey: '0x...',
144
+ rpcUrl: '...',
145
+ subgraphUrl: 'https://your-subgraph.example.com/graphql',
146
+ builderApiUrl: 'https://your-builder-api.example.com',
147
+ });
148
+
149
+ const reader = await OstiumClient.createReadOnly({
150
+ subgraphUrl: 'https://your-subgraph.example.com/graphql',
151
+ builderApiUrl: 'https://your-builder-api.example.com',
152
+ });
153
+ ```
154
+
155
+ ### Self + Gasless: One-Time Setup
156
+
157
+ A Safe smart account is derived deterministically from your private key. You need to register it as your on-chain delegate once:
158
+
159
+ ```ts
160
+ const client = await OstiumClient.createSelfAndGasless({
161
+ traderPrivateKey: '0x...',
162
+ pimlicoUrl: 'https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161',
163
+ });
164
+
165
+ console.log('EOA (trader):', client.getTraderAddress());
166
+ console.log('Safe (delegate):', client.getSmartAccountAddress());
167
+
168
+ await client.approveUsdc('max'); // EOA → TradingStorage approval (gas, once)
169
+ await client.setupGaslessDelegation(); // EOA → setDelegate(Safe) (gas, once)
170
+
171
+ // All trades from here are gasless UserOps
172
+ await client.openTrade({ ... });
173
+ ```
174
+
175
+ ### Delegated Modes: Setup Checklist
176
+
177
+ The trader account must perform two one-time operations before the delegate can trade:
178
+
179
+ ```ts
180
+ // 1. Trader approves USDC
181
+ const traderClient = await OstiumClient.createSelfAndSelf({
182
+ traderPrivateKey: '0xTraderKey',
183
+ rpcUrl: '...',
184
+ });
185
+ await traderClient.approveUsdc('max');
186
+
187
+ // 2a. Delegated + Self: trader registers the delegate EOA
188
+ await traderClient.setDelegate('0xDelegateEOA');
189
+
190
+ // 2b. Delegated + Gasless: trader registers the delegate's Safe
191
+ const delegateClient = await OstiumClient.createDelegatedAndGasless({
192
+ delegatePrivateKey: '0xDelegateKey',
193
+ traderAddress: '0xTraderAddress',
194
+ pimlicoUrl: '...',
195
+ });
196
+ await traderClient.setDelegate(delegateClient.getSmartAccountAddress()!);
197
+ ```
198
+
199
+ ---
200
+
201
+ ## CLI — Environment Variable Configuration
202
+
203
+ 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:
204
+
205
+ ```dotenv
206
+ # ── Universal ─────────────────────────────────────────────────────────────────
207
+
208
+ # Self / main EOA private key (required for Self modes; also used as the main key in Test + Delegated)
209
+ PRIVATE_KEY=0x...
210
+
211
+ # Arbitrum RPC URL — required for Self+Self and Delegated+Self; optional for gasless modes
212
+ ARB_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
213
+
214
+ # ── Delegated modes ───────────────────────────────────────────────────────────
215
+
216
+ # Delegate EOA private key (falls back to PRIVATE_KEY if not set)
217
+ DELEGATE_PRIVATE_KEY=0x...
218
+
219
+ # Trader address the delegate acts on behalf of (required for Trade + Delegated)
220
+ TRADER_ADDRESS=0x...
221
+
222
+ # ── Gasless modes ─────────────────────────────────────────────────────────────
223
+
224
+ # Pimlico sponsor URL (required for Self+Gasless and Delegated+Gasless)
225
+ PIMLICO_URL=https://builder.ostium.io/v1/pimlico/sponsor?chainId=42161
226
+ ```
227
+
228
+ 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.
229
+
230
+ ---
231
+
232
+ ## Networks
233
+
234
+ | Network | Chain ID | `testnet` |
235
+ |:--|:-:|:-:|
236
+ | Arbitrum One | 42161 | `false` (default) |
237
+ | Arbitrum Sepolia | 421614 | `true` |
238
+
239
+ ```ts
240
+ const client = await OstiumClient.createSelfAndSelf({
241
+ traderPrivateKey: '0x...',
242
+ rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc',
243
+ testnet: true,
244
+ });
245
+ ```
246
+
247
+ ---
248
+
249
+ ## API Reference
250
+
251
+ ### Enums
252
+
253
+ ```ts
254
+ import { OrderType, CancelOrderType } from '@ostium/builder-sdk';
255
+
256
+ OrderType.Market // 'market'
257
+ OrderType.Limit // 'limit'
258
+ OrderType.Stop // 'stop'
259
+
260
+ CancelOrderType.Limit // 'limit'
261
+ CancelOrderType.PendingOpen // 'pendingOpen'
262
+ CancelOrderType.PendingClose // 'pendingClose'
263
+ ```
264
+
265
+ ### Trading Methods
266
+
267
+ 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.
268
+
269
+ ```ts
270
+ interface SubmissionResult {
271
+ txHash: `0x${string}`;
272
+ smartAccountAddress?: `0x${string}`; // gasless modes only
273
+ }
274
+ ```
275
+
276
+ Use `getOrders({ initiatedTxHashes: [result.txHash] })` to poll by submission hash, or `orderIds` / recent orders for the trader.
277
+
278
+ #### `openTrade(params)`
279
+
280
+ ```ts
281
+ const result = await client.openTrade({
282
+ pairIndex: 0, // trading pair (use getPairs() to list all)
283
+ buy: true, // true = long, false = short
284
+ price: '65000', // entry price
285
+ collateral: '100', // USD (min $5)
286
+ leverage: '10', // multiplier (max varies by pair)
287
+ type: OrderType.Market,
288
+ takeProfit?: '70000',
289
+ stopLoss?: '60000',
290
+ slippage?: 50, // bps, market orders only
291
+ isDayTrade?: false, // true when leverage > pair.overnightMaxLeverage
292
+ });
293
+ console.log(result.txHash);
294
+ ```
295
+
296
+ > **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.
297
+
298
+ #### `closeTrade(params)`
299
+
300
+ `pairId` and `idx` come from `getOpenPositions()`:
301
+
302
+ ```ts
303
+ const { pairPositions } = await client.getOpenPositions();
304
+ const { pairId, idx } = pairPositions[0].position;
305
+
306
+ await client.closeTrade({
307
+ pairId, // from Position.pairId
308
+ idx, // from Position.idx
309
+ price: '66000',
310
+ closePercent: 100, // 1–100, partial closes supported
311
+ slippage?: 50,
312
+ });
313
+ ```
314
+
315
+ #### `cancelOrder(params)`
316
+
317
+ TypeScript narrows the required fields by `type`:
318
+
319
+ ```ts
320
+ // Cancel a pending limit order
321
+ const [order] = await client.getOpenOrders();
322
+ await client.cancelOrder({ type: CancelOrderType.Limit, pairId: order.pairId, idx: order.idx });
323
+
324
+ // Cancel a timed-out market open (orderId from chain / indexer)
325
+ await client.cancelOrder({ type: CancelOrderType.PendingOpen, orderId: 123 });
326
+
327
+ // Cancel a timed-out market close — retry: true re-submits the close
328
+ await client.cancelOrder({ type: CancelOrderType.PendingClose, orderId: 123, retry: true });
329
+ ```
330
+
331
+ #### `modifyOrder(params)`
332
+
333
+ `pairId` and `idx` come from `getOpenPositions()` (for TP/SL) or `getOpenOrders()` (for limit order price):
334
+
335
+ ```ts
336
+ // Update TP on an open trade
337
+ await client.modifyOrder({ pairId, idx, takeProfit: '72000' });
338
+
339
+ // Update SL on an open trade
340
+ await client.modifyOrder({ pairId, idx, stopLoss: '61000' });
341
+
342
+ // Reprice a limit order (with optional new TP/SL)
343
+ await client.modifyOrder({ pairId, idx, price: '64000', takeProfit: '70000' });
344
+ ```
345
+
346
+ Note: you cannot update both `takeProfit` and `stopLoss` in a single call without also providing `price`. Send two separate calls instead.
347
+
348
+ #### `updateCollateral(params)`
349
+
350
+ `pairId` and `idx` come from `getOpenPositions()`:
351
+
352
+ ```ts
353
+ await client.updateCollateral({ pairId, idx, amount: '50' }); // add $50
354
+ await client.updateCollateral({ pairId, idx, amount: '-30' }); // remove $30
355
+ ```
356
+
357
+ ### USDC + Balance Helpers
358
+
359
+ ```ts
360
+ const { current, required, sufficient } = await client.checkUsdcAllowance('100');
361
+ await client.approveUsdc('1000'); // approve $1000
362
+ await client.approveUsdc('max'); // approve MaxUint256
363
+
364
+ // All three values are decimal strings produced by formatUnits (e.g. "1234.56")
365
+ // Parse with Number() / parseFloat() only when you need numeric arithmetic.
366
+ const { usdc, eth, allowance } = await client.getBalances();
367
+ // In read-only mode, pass the address explicitly:
368
+ const { usdc, eth, allowance } = await client.getBalances('0xTrader...');
369
+ ```
370
+
371
+ ### Read Methods
372
+
373
+ All read methods are available on both `OstiumClient` and `OstiumSubgraphClient`. On `OstiumClient` the connected trader address is used by default wherever `user` is optional.
374
+
375
+ #### `getPairs(params?)`
376
+
377
+ All trading pairs with live prices, market-status flags, computed size limits, and rollover rates.
378
+
379
+ ```ts
380
+ const { pairs } = await client.getPairs();
381
+ // Optional — filter to specific pairs:
382
+ const { pairs } = await client.getPairs({ pairIds: [0, 1, 4] });
383
+
384
+ // Pair fields:
385
+ // pairId, pairTo, pairFrom, category
386
+ // maxLeverage, overnightMaxLeverage
387
+ // minSz, maxBSz, maxSSz, minNtl
388
+ // openInterest, buyOpenInterest, sellOpenInterest, maxOpenInterest
389
+ // rolloverRate: { long, short } — 8hr % by side
390
+ // rolloverFeePerBlock
391
+ // midPx, askPx, bidPx
392
+ // isMarketOpen, isDayTradingClosed, secondsToToggleIsDayTradingClosed
393
+ ```
394
+
395
+ Pair symbol names from the subgraph are normalized automatically (e.g. CL → WTI, FTSE → UK100, SPX → US500).
396
+
397
+ #### `getAllPrices()`
398
+
399
+ Live mid/bid/ask prices for every pair, keyed by `pairId`.
400
+
401
+ ```ts
402
+ const { prices } = await client.getAllPrices();
403
+ // prices['0'] → { mid: '65000', bid: '64990', ask: '65010' }
404
+ ```
405
+
406
+ #### `getOpenPositions(params?)`
407
+
408
+ Open positions, margin summary, and per-position live PnL. Block number is fetched automatically on `OstiumClient`.
409
+
410
+ ```ts
411
+ const { pairPositions, marginSummary, withdrawable, time } =
412
+ await client.getOpenPositions();
413
+
414
+ // Each position:
415
+ const { pairId, pid, idx, side, szi, entryPx, leverage, ntl,
416
+ unrealizedPnl, returnOnEquity, liquidationPx,
417
+ collateralUsed, cumRollover, tpPx, slPx,
418
+ openTimestamp, isDayTrade } = pairPositions[0].position;
419
+
420
+ // marginSummary: accountValue, totalCollateralUsed, totalNtlPos, totalRawPnlUsd
421
+ // withdrawable: max collateral removable across all positions
422
+ // time: server time (Unix ms)
423
+ ```
424
+
425
+ #### `getFills(params?)`
426
+
427
+ Executed fills, newest first.
428
+
429
+ ```ts
430
+ const fills = await client.getFills();
431
+ // Filter options:
432
+ const fills = await client.getFills({ pairId: 0, limit: 50 });
433
+ // All traders (no address filter):
434
+ const fills = await client.getFills({ user: 'ALL' });
435
+ ```
436
+
437
+ #### `getFillsByTime(params)`
438
+
439
+ Same as `getFills` but with a time range filter (Unix milliseconds).
440
+
441
+ ```ts
442
+ const fills = await client.getFillsByTime({
443
+ startTime: Date.now() - 7 * 86_400_000, // last 7 days
444
+ endTime: Date.now(), // optional, defaults to now
445
+ pairId: 0, // optional pair filter
446
+ });
447
+ ```
448
+
449
+ #### `getOpenOrders(params?)`
450
+
451
+ Active limit orders for a trader.
452
+
453
+ ```ts
454
+ const orders = await client.getOpenOrders();
455
+ // Each order: pairId, pairTo, pairFrom, idx, side, limitPx, szi, orderType, tpPx?, slPx?, timestamp
456
+ ```
457
+
458
+ #### `getOrders(params?)`
459
+
460
+ Orders at any status — pending, executed, or cancelled. Use this to poll whether an `openTrade`/`closeTrade` was executed by the oracle.
461
+
462
+ ```ts
463
+ const result = await client.openTrade({ /* … */ });
464
+
465
+ // Poll by the submission tx (matches subgraph `initiatedTx`):
466
+ const [order] = await client.getOrders({ initiatedTxHashes: [result.txHash] });
467
+ console.log(order.isPending, order.isCancelled);
468
+
469
+ // Or by on-chain order id:
470
+ const [byId] = await client.getOrders({ orderIds: [123] });
471
+
472
+ // Recent orders for the connected trader (no filter args):
473
+ const orders = await client.getOrders();
474
+ ```
475
+
476
+ Each `Order` extends `Fill` with `initiatedTx`, `initiatedTime`, `isPending`, `isCancelled`, and optional `cancelReason`.
477
+
478
+ #### `getSimSlippage(params)`
479
+
480
+ Simulate price impact for a list of pairs and notionals.
481
+
482
+ ```ts
483
+ const result = await client.getSimSlippage({
484
+ pairIds: [0, 1],
485
+ ntls: ['1000', '10000', '100000'],
486
+ });
487
+ // result['0'].long → [{ ntl, slippage }, ...]
488
+ // result['0'].short → [{ ntl, slippage }, ...]
489
+ ```
490
+
491
+ #### `getSimOrderbook(params)`
492
+
493
+ 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.
494
+
495
+ ```ts
496
+ const book = await client.getSimOrderbook({ pairId: 0, levels: 20 });
497
+ // book.levels[0] → bids (short entries), best bid first
498
+ // book.levels[1] → asks (long entries), best ask first
499
+ // Each level: { px: string, sz: string, n: 1 }
500
+ ```
501
+
502
+ #### `getCandles(params)`
503
+
504
+ OHLC candles from the builder API. `from`/`to` are Unix milliseconds. `pairId` is the same value returned by `getPairs()`.
505
+
506
+ ```ts
507
+ const candles = await client.getCandles({
508
+ pairId: 0,
509
+ from: Date.now() - 30 * 86_400_000, // last 30 days
510
+ to: Date.now(), // optional, defaults to now
511
+ resolution: '1D', // '1' | '5' | '15' | '60' | '240' | '1D'
512
+ });
513
+ // Each candle: { pairFrom, pairTo, time, open, high, low, close }
514
+ // pairFrom / pairTo use normalized display names (e.g. "WTI" not "CL")
515
+ ```
516
+
517
+ #### `streamPrices(pairIds?)`
518
+
519
+ 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.
520
+
521
+ ```ts
522
+ const stream = client.streamPrices([0, 1]); // BTC and ETH by pairId
523
+
524
+ stream.onOpen(() => console.log('connected'));
525
+ stream.onSnapshot(ticks => console.log('initial snapshot:', ticks.length));
526
+ stream.onTick(tick => console.log(tick.pair, tick.mid));
527
+ // tick.pair / tick.from / tick.to use normalized display names
528
+
529
+ // Dynamically add / remove by pairId:
530
+ stream.subscribe([2]);
531
+ stream.unsubscribe([0]);
532
+
533
+ // Clean up:
534
+ stream.close();
535
+ ```
536
+
537
+ Requires Node.js 18+, Bun, or a browser environment.
538
+
539
+ ### Getters
540
+
541
+ ```ts
542
+ client.getTraderAddress(); // on-chain trader address (throws in read-only mode)
543
+ client.getSmartAccountAddress(); // Safe address — gasless modes only, else undefined
544
+ client.isReadOnly(); // true when created via createReadOnly()
545
+ ```
546
+
547
+ ### Error Codes
548
+
549
+ ```ts
550
+ import { OstiumError, OstiumErrorCode } from '@ostium/builder-sdk';
551
+ ```
552
+
553
+ | Code | When thrown |
554
+ |:--|:--|
555
+ | `INVALID_CONFIG` | Missing or malformed config (privateKey format, address format, fee range) |
556
+ | `VALIDATION_FAILED` | Invalid trade params (TP/SL direction, leverage range, close percent) |
557
+ | `ALLOWANCE_INSUFFICIENT` | USDC allowance too low for `openTrade` / `updateCollateral` |
558
+ | `DELEGATION_FAILED` | `approveUsdc()` called in delegated mode |
559
+ | `CONTRACT_ERROR` | Contract revert (e.g. `WrongLeverage`, `NoDelegate`) |
560
+ | `NETWORK_ERROR` | RPC / Pimlico connectivity issues or rate limits |
561
+ | `SUBMISSION_FAILED` | Transaction rejected for other reasons |
562
+
563
+ ### Decimal Utilities
564
+
565
+ ```ts
566
+ import { parseUsdc, parsePrice, parseLeverage, MIN_COLLATERAL_USD, MAX_COLLATERAL_USD } from '@ostium/builder-sdk';
567
+
568
+ parsePrice('65000.50'); // 65000500000000000000000n (18 decimals)
569
+ parseUsdc('100.50'); // 100500000n (6 decimals)
570
+ MIN_COLLATERAL_USD; // 5
571
+ MAX_COLLATERAL_USD; // 2_000_000
572
+ ```
573
+
574
+ ### Standalone Subgraph Client
575
+
576
+ For read-only market data without a trading key, use `OstiumSubgraphClient` directly:
577
+
578
+ ```ts
579
+ import { OstiumSubgraphClient } from '@ostium/builder-sdk';
580
+
581
+ const subgraph = await OstiumSubgraphClient.create({ testnet: false });
582
+
583
+ const { pairs } = await subgraph.getPairs();
584
+ const { prices } = await subgraph.getAllPrices();
585
+ const positions = await subgraph.getOpenPositions({ user: '0xTrader...' });
586
+ const fills = await subgraph.getFills({ user: '0xTrader...', limit: 50 });
587
+ ```
588
+
589
+ `OstiumSubgraphClient.getOpenPositions` accepts an optional `blockNumber` (required for accurate PnL). On `OstiumClient` this is fetched automatically.