openbroker 1.0.85 → 1.0.88

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/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
4
4
  license: MIT
5
5
  compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
6
6
  homepage: https://www.npmjs.com/package/openbroker
7
- metadata: {"author": "monemetrics", "version": "1.0.85", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.88", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
8
8
  allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_spot_buy ob_spot_sell ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.85",
4
+ "version": "1.0.88",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.85",
3
+ "version": "1.0.88",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -338,12 +338,12 @@ function renderTextReport(data: ReturnType<typeof loadReport>, watchMode = false
338
338
  console.log('\nEquity');
339
339
  console.log('------');
340
340
  if (report.equity.first) {
341
- console.log(`First snapshot: ${formatUsd(report.equity.first.equity)} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
341
+ console.log(`First snapshot: ${formatUsd(Number(report.equity.first.equity))} @ ${formatTimestamp(Number(report.equity.first.timestamp))}`);
342
342
  } else {
343
343
  console.log('First snapshot: -');
344
344
  }
345
345
  if (report.equity.latest) {
346
- console.log(`Latest snapshot:${formatUsd(report.equity.latest.equity)} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
346
+ console.log(`Latest snapshot:${formatUsd(Number(report.equity.latest.equity))} @ ${formatTimestamp(Number(report.equity.latest.timestamp))}`);
347
347
  } else {
348
348
  console.log('Latest snapshot:-');
349
349
  }
@@ -569,21 +569,19 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
569
569
  }, 'ws');
570
570
  }
571
571
 
572
- // Also emit order_filled for backward compatibility
573
- if (update.status === 'filled' && eventBus.has('order_filled')) {
574
- void emitAutomationEvent('order_filled', {
575
- coin: update.order.coin,
576
- oid: update.order.oid,
577
- side: update.order.side === 'B' ? 'buy' : 'sell',
578
- size: parseFloat(update.order.sz),
579
- price: parseFloat(update.order.limitPx),
580
- }, 'ws');
581
- }
572
+ // NOTE: order_filled is emitted from the userFill handler below, not from
573
+ // here. The previous implementation fired it from orderUpdate.status
574
+ // === 'filled' using update.order.sz as the size, but that field is the
575
+ // REMAINING size (0 on a terminal fill), not the fill delta — so every
576
+ // consumer saw size=0. Additionally, Hyperliquid does not emit
577
+ // orderUpdate events for pure partial fills that don't transition
578
+ // status, so partial fills were silently dropped entirely. Sourcing
579
+ // order_filled from userFill fixes both issues: sz there IS the fill
580
+ // delta, and the userFills stream fires on every fill (partial and
581
+ // terminal).
582
582
  });
583
583
 
584
584
  ws.on('userFill', (fill) => {
585
- // userFill events are already covered by order_update with status=filled
586
- // But this provides the realized PnL and fee data that order_update doesn't have
587
585
  audit.recordFill({
588
586
  coin: fill.coin,
589
587
  side: fill.side === 'B' ? 'buy' : 'sell',
@@ -596,6 +594,40 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
596
594
  crossed: fill.crossed,
597
595
  }, fill.time);
598
596
  log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
597
+
598
+ // Emit order_filled with the authoritative fill delta + fee/pnl from
599
+ // the userFills WS stream. Covers both partial and terminal fills.
600
+ // Fee is converted to USD using feeToken: for non-USDC fees (spot
601
+ // buys pay in the received asset), fee × price yields USD since the
602
+ // fee token is the base of the traded pair and `price` is quote/base.
603
+ if (eventBus.has('order_filled')) {
604
+ const size = parseFloat(fill.sz);
605
+ const price = parseFloat(fill.px);
606
+ const rawFee = parseFloat(fill.fee);
607
+ const closedPnl = parseFloat(fill.closedPnl);
608
+ const feeToken = fill.feeToken;
609
+ let feeUsd: number | undefined;
610
+ if (Number.isFinite(rawFee)) {
611
+ if (feeToken === 'USDC' || !feeToken) {
612
+ feeUsd = rawFee;
613
+ } else if (Number.isFinite(price) && price > 0) {
614
+ feeUsd = rawFee * price;
615
+ } else {
616
+ feeUsd = undefined;
617
+ }
618
+ }
619
+ void emitAutomationEvent('order_filled', {
620
+ coin: fill.coin,
621
+ oid: fill.oid,
622
+ side: fill.side === 'B' ? 'buy' : 'sell',
623
+ size,
624
+ price,
625
+ fee: feeUsd,
626
+ feeToken,
627
+ closedPnl: Number.isFinite(closedPnl) ? closedPnl : undefined,
628
+ crossed: fill.crossed,
629
+ }, 'ws');
630
+ }
599
631
  });
600
632
 
601
633
  ws.on('userEvent', (event) => {
@@ -46,7 +46,29 @@ export interface AutomationEventPayloads {
46
46
  position_changed: { coin: string; oldSize: number; newSize: number; entryPrice: number };
47
47
  pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number };
48
48
  margin_warning: { marginUsedPct: number; equity: number; marginUsed: number };
49
- order_filled: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number };
49
+ /**
50
+ * Fires on every trade fill — partial and terminal — sourced from the
51
+ * Hyperliquid `userFills` WS stream. `size` is the fill delta (NOT remaining
52
+ * size of the order). `fee` and `closedPnl` are in USD (converted from the
53
+ * raw fee denomination using `price` when `feeToken !== "USDC"`, which
54
+ * happens on spot buys where the fee is charged in the received asset).
55
+ * `feeToken` is the original denomination so consumers can identify legs
56
+ * that won't carry a builder fee (Hyperliquid only charges builder fees on
57
+ * USDC-denominated trades). `crossed` is true when this side was the taker.
58
+ * Fee/pnl/crossed/feeToken are optional so older consumers that only read
59
+ * coin/oid/side/size/price keep working.
60
+ */
61
+ order_filled: {
62
+ coin: string;
63
+ oid: number;
64
+ side: 'buy' | 'sell';
65
+ size: number;
66
+ price: number;
67
+ fee?: number;
68
+ feeToken?: string;
69
+ closedPnl?: number;
70
+ crossed?: boolean;
71
+ };
50
72
  /** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */
51
73
  order_update: {
52
74
  coin: string;
@@ -9,6 +9,8 @@ import type {
9
9
  OrderResponse,
10
10
  CancelResponse,
11
11
  MetaAndAssetCtxs,
12
+ AssetMeta,
13
+ AssetCtx,
12
14
  ClearinghouseState,
13
15
  OpenOrder,
14
16
  } from './types.js';
@@ -256,13 +258,16 @@ export class HyperliquidClient {
256
258
  }
257
259
  this.log('metaAndAssetCtxs response:', JSON.stringify(response, null, 2).slice(0, 500) + '...');
258
260
 
259
- this.meta = {
260
- meta: { universe: response[0].universe },
261
- assetCtxs: response[1],
261
+ // Build the narrowed value locally so neither narrowing nor the `| null`
262
+ // field type can be lost across `await` / method-call boundaries below.
263
+ const meta: MetaAndAssetCtxs = {
264
+ meta: { universe: response[0].universe as AssetMeta[] },
265
+ assetCtxs: response[1] as AssetCtx[],
262
266
  };
267
+ this.meta = meta;
263
268
 
264
269
  // Build lookup maps for main dex
265
- this.meta.meta.universe.forEach((asset, index) => {
270
+ meta.meta.universe.forEach((asset, index) => {
266
271
  this.assetMap.set(asset.name, index);
267
272
  this.szDecimalsMap.set(asset.name, asset.szDecimals);
268
273
  this.coinDexMap.set(asset.name, { dexName: null, dexIdx: 0, localName: asset.name });
@@ -274,7 +279,7 @@ export class HyperliquidClient {
274
279
  this.hip3Loaded = true;
275
280
  }
276
281
 
277
- return this.meta;
282
+ return meta;
278
283
  }
279
284
 
280
285
  /**
@@ -782,7 +787,19 @@ export class HyperliquidClient {
782
787
  tokenId,
783
788
  }),
784
789
  });
785
- const data = await response.json();
790
+ const data = await response.json() as {
791
+ name: string;
792
+ maxSupply: string;
793
+ totalSupply: string;
794
+ circulatingSupply: string;
795
+ szDecimals: number;
796
+ weiDecimals: number;
797
+ midPx: string;
798
+ markPx: string;
799
+ prevDayPx: string;
800
+ deployer: string;
801
+ deployTime: string;
802
+ } | null;
786
803
  this.log('tokenDetails response:', JSON.stringify(data).slice(0, 500));
787
804
  return data;
788
805
  } catch {
@@ -807,7 +824,10 @@ export class HyperliquidClient {
807
824
  headers: { 'Content-Type': 'application/json' },
808
825
  body: JSON.stringify({ type: 'predictedFundings' }),
809
826
  });
810
- const data = await response.json();
827
+ const data = await response.json() as Array<[
828
+ string,
829
+ Array<[string, { fundingRate: string; nextFundingTime: number }]>
830
+ ]>;
811
831
  this.log('predictedFundings response length:', data?.length);
812
832
  return data;
813
833
  }
@@ -827,7 +847,7 @@ export class HyperliquidClient {
827
847
  }> {
828
848
  this.log('Fetching l2Book for:', coin);
829
849
  // API accepts prefixed names directly (e.g., "xyz:CL")
830
- let response;
850
+ let response: Awaited<ReturnType<typeof this.info.l2Book>>;
831
851
  try {
832
852
  response = await this.info.l2Book({ coin });
833
853
  } catch (error) {
@@ -838,8 +858,12 @@ export class HyperliquidClient {
838
858
  throw new Error(`l2Book(${coin}) failed: ${this.describeError(error)}`);
839
859
  }
840
860
 
841
- const bids = response.levels[0] as Array<{ px: string; sz: string; n: number }>;
842
- const asks = response.levels[1] as Array<{ px: string; sz: string; n: number }>;
861
+ if (!response || !Array.isArray(response.levels)) {
862
+ throw new Error(`l2Book(${coin}) returned empty/malformed payload.`);
863
+ }
864
+
865
+ const bids = (response.levels[0] ?? []) as Array<{ px: string; sz: string; n: number }>;
866
+ const asks = (response.levels[1] ?? []) as Array<{ px: string; sz: string; n: number }>;
843
867
 
844
868
  const bestBid = bids.length > 0 ? parseFloat(bids[0].px) : 0;
845
869
  const bestAsk = asks.length > 0 ? parseFloat(asks[0].px) : 0;
@@ -1015,7 +1039,7 @@ export class HyperliquidClient {
1015
1039
  user: user ?? this.address,
1016
1040
  }),
1017
1041
  });
1018
- const data = await response.json();
1042
+ const data = await response.json() as string | { abstraction?: string; mode?: string } | null;
1019
1043
  this.log('userAbstraction response:', JSON.stringify(data));
1020
1044
 
1021
1045
  // API may return a bare string or an object. Normalize to string for matching.
@@ -1194,10 +1218,13 @@ export class HyperliquidClient {
1194
1218
  }
1195
1219
 
1196
1220
  try {
1221
+ // approveBuilderFee is a wallet-level authorization (not a vault action)
1222
+ // and the SDK's `opts` parameter only carries an AbortSignal. Do not pass
1223
+ // vaultParam here — the signature is { signal? }, not a vault wrapper.
1197
1224
  const response = await this.exchange.approveBuilderFee({
1198
1225
  builder: targetBuilder as `0x${string}`,
1199
1226
  maxFeeRate,
1200
- }, this.vaultParam);
1227
+ });
1201
1228
  this.log('approveBuilderFee response:', response);
1202
1229
  return { status: 'ok', response };
1203
1230
  } catch (error) {
@@ -1242,7 +1269,18 @@ export class HyperliquidClient {
1242
1269
  headers: { 'Content-Type': 'application/json' },
1243
1270
  body: JSON.stringify(body),
1244
1271
  });
1245
- const data = await response.json();
1272
+ const data = await response.json() as Array<{
1273
+ time: number;
1274
+ hash: string;
1275
+ delta: {
1276
+ type: 'funding';
1277
+ coin: string;
1278
+ usdc: string;
1279
+ szi: string;
1280
+ fundingRate: string;
1281
+ nSamples: number | null;
1282
+ };
1283
+ }>;
1246
1284
  this.log('userFunding response length:', data?.length);
1247
1285
  return data;
1248
1286
  }
@@ -1285,7 +1323,25 @@ export class HyperliquidClient {
1285
1323
  headers: { 'Content-Type': 'application/json' },
1286
1324
  body: JSON.stringify(body),
1287
1325
  });
1288
- const data = await response.json();
1326
+ const data = await response.json() as Array<{
1327
+ coin: string;
1328
+ px: string;
1329
+ sz: string;
1330
+ side: 'B' | 'A';
1331
+ time: number;
1332
+ startPosition: string;
1333
+ dir: string;
1334
+ closedPnl: string;
1335
+ fee: string;
1336
+ hash: string;
1337
+ oid: number;
1338
+ tid: number;
1339
+ crossed: boolean;
1340
+ feeToken: string;
1341
+ twapId: number | null;
1342
+ cloid: string | null;
1343
+ builderFee: string | null;
1344
+ }>;
1289
1345
  this.log('userFills response length:', data?.length);
1290
1346
  return data;
1291
1347
  }
@@ -1328,7 +1384,28 @@ export class HyperliquidClient {
1328
1384
  user: user ?? this.address,
1329
1385
  }),
1330
1386
  });
1331
- const data = await response.json();
1387
+ const data = await response.json() as Array<{
1388
+ order: {
1389
+ coin: string;
1390
+ side: string;
1391
+ limitPx: string;
1392
+ sz: string;
1393
+ origSz: string;
1394
+ oid: number;
1395
+ timestamp: number;
1396
+ orderType: string;
1397
+ tif: string | null;
1398
+ cloid: string | null;
1399
+ triggerCondition: string;
1400
+ triggerPx: string;
1401
+ isTrigger: boolean;
1402
+ isPositionTpsl: boolean;
1403
+ reduceOnly: boolean;
1404
+ children: unknown[];
1405
+ };
1406
+ status: string;
1407
+ statusTimestamp: number;
1408
+ }>;
1332
1409
  this.log('historicalOrders response length:', data?.length);
1333
1410
  return data;
1334
1411
  }
@@ -1374,7 +1451,30 @@ export class HyperliquidClient {
1374
1451
  oid: typeof oid === 'string' ? oid : oid,
1375
1452
  }),
1376
1453
  });
1377
- const data = await response.json();
1454
+ const data = await response.json() as {
1455
+ status: string;
1456
+ order?: {
1457
+ order: {
1458
+ coin: string;
1459
+ side: string;
1460
+ limitPx: string;
1461
+ sz: string;
1462
+ origSz: string;
1463
+ oid: number;
1464
+ timestamp: number;
1465
+ orderType: string;
1466
+ tif: string | null;
1467
+ cloid: string | null;
1468
+ triggerCondition: string;
1469
+ triggerPx: string;
1470
+ isTrigger: boolean;
1471
+ isPositionTpsl: boolean;
1472
+ reduceOnly: boolean;
1473
+ };
1474
+ status: string;
1475
+ statusTimestamp: number;
1476
+ };
1477
+ };
1378
1478
  this.log('orderStatus response:', JSON.stringify(data).slice(0, 500));
1379
1479
  return data;
1380
1480
  }
@@ -1409,7 +1509,20 @@ export class HyperliquidClient {
1409
1509
  user: user ?? this.address,
1410
1510
  }),
1411
1511
  });
1412
- const data = await response.json();
1512
+ const data = await response.json() as {
1513
+ dailyUserVlm: Array<{ date: string; exchange: string; userCross: string; userAdd: string }>;
1514
+ feeSchedule: Record<string, unknown>;
1515
+ userCrossRate: string;
1516
+ userAddRate: string;
1517
+ userSpotCrossRate: string;
1518
+ userSpotAddRate: string;
1519
+ activeReferralDiscount: string;
1520
+ trial: unknown;
1521
+ feeTrialEscrow: string;
1522
+ nextTrialAvailableTimestamp: unknown;
1523
+ stakingLink: { stakingUser: string; status: string } | null;
1524
+ activeStakingDiscount: { basisPoints: number; discountRate: string } | null;
1525
+ };
1413
1526
  this.log('userFees response:', JSON.stringify(data).slice(0, 500));
1414
1527
  return data;
1415
1528
  }
@@ -1448,7 +1561,18 @@ export class HyperliquidClient {
1448
1561
  headers: { 'Content-Type': 'application/json' },
1449
1562
  body: JSON.stringify({ type: 'candleSnapshot', req }),
1450
1563
  });
1451
- const data = await response.json();
1564
+ const data = await response.json() as Array<{
1565
+ t: number;
1566
+ T: number;
1567
+ s: string;
1568
+ i: string;
1569
+ o: string;
1570
+ c: string;
1571
+ h: string;
1572
+ l: string;
1573
+ v: string;
1574
+ n: number;
1575
+ }>;
1452
1576
  this.log('candleSnapshot response length:', data?.length);
1453
1577
  return data;
1454
1578
  }
@@ -1480,7 +1604,12 @@ export class HyperliquidClient {
1480
1604
  headers: { 'Content-Type': 'application/json' },
1481
1605
  body: JSON.stringify(body),
1482
1606
  });
1483
- const data = await response.json();
1607
+ const data = await response.json() as Array<{
1608
+ coin: string;
1609
+ fundingRate: string;
1610
+ premium: string;
1611
+ time: number;
1612
+ }>;
1484
1613
  this.log('fundingHistory response length:', data?.length);
1485
1614
  return data;
1486
1615
  }
@@ -1510,7 +1639,15 @@ export class HyperliquidClient {
1510
1639
  headers: { 'Content-Type': 'application/json' },
1511
1640
  body: JSON.stringify(body),
1512
1641
  });
1513
- const data = await response.json();
1642
+ const data = await response.json() as Array<{
1643
+ coin: string;
1644
+ side: 'B' | 'A';
1645
+ px: string;
1646
+ sz: string;
1647
+ time: number;
1648
+ hash: string;
1649
+ tid: number;
1650
+ }>;
1514
1651
  this.log('recentTrades response length:', data?.length);
1515
1652
  return data;
1516
1653
  }
@@ -1537,7 +1674,12 @@ export class HyperliquidClient {
1537
1674
  user: user ?? this.address,
1538
1675
  }),
1539
1676
  });
1540
- const data = await response.json();
1677
+ const data = await response.json() as {
1678
+ cumVlm: string;
1679
+ nRequestsUsed: number;
1680
+ nRequestsCap: number;
1681
+ nRequestsSurplus: number;
1682
+ };
1541
1683
  this.log('userRateLimit response:', JSON.stringify(data));
1542
1684
  return data;
1543
1685
  }
@@ -2137,7 +2279,9 @@ export class HyperliquidClient {
2137
2279
  // Use the exact spot market key from spotMeta (e.g. "@230", "PURR/USDC").
2138
2280
  // On testnet the tradable asset id and displayed market key can diverge.
2139
2281
  const mids = await this.getAllMids();
2140
- let midStr = mids[spotCoinKey];
2282
+ // Record<string, string> lookup returns `string` under TS defaults but
2283
+ // is runtime-undefined when the key is absent — hence the explicit union.
2284
+ let midStr: string | undefined = mids[spotCoinKey];
2141
2285
 
2142
2286
  // Fallback: allMids may omit spot pairs (especially on testnet).
2143
2287
  // Try spotMetaAndAssetCtxs which returns markPx directly.
@@ -2295,7 +2439,7 @@ export class HyperliquidClient {
2295
2439
  await this.getMetaAndAssetCtxs();
2296
2440
 
2297
2441
  if (leverage) {
2298
- await this.setLeverage(coin, leverage);
2442
+ await this.updateLeverage(coin, leverage);
2299
2443
  }
2300
2444
 
2301
2445
  const assetIndex = this.getAssetIndex(coin);
@@ -81,11 +81,18 @@ export interface CancelResponse {
81
81
 
82
82
  // ============ Market Data Types ============
83
83
 
84
+ // Shape matches the upstream @nktkas/hyperliquid SDK response. Fields like
85
+ // `onlyIsolated` and `premium` are absent-or-null in practice for some assets,
86
+ // so they're modelled as optional / nullable. Index signature absorbs any
87
+ // additional SDK fields we don't consume here (marginTableId, growthMode, …).
84
88
  export interface AssetMeta {
85
89
  name: string;
86
90
  szDecimals: number;
87
91
  maxLeverage: number;
88
- onlyIsolated: boolean;
92
+ onlyIsolated?: boolean;
93
+ isDelisted?: boolean;
94
+ marginTableId?: number;
95
+ [key: string]: unknown;
89
96
  }
90
97
 
91
98
  export interface AssetCtx {
@@ -93,11 +100,13 @@ export interface AssetCtx {
93
100
  openInterest: string;
94
101
  prevDayPx: string;
95
102
  dayNtlVlm: string;
96
- premium: string;
103
+ premium: string | null;
97
104
  oraclePx: string;
98
105
  markPx: string;
99
- midPx?: string;
100
- impactPxs?: [string, string];
106
+ midPx?: string | null;
107
+ impactPxs?: [string, string] | string[] | null;
108
+ dayBaseVlm?: string;
109
+ [key: string]: unknown;
101
110
  }
102
111
 
103
112
  export interface Meta {
@@ -51,6 +51,8 @@ export interface WsEventMap {
51
51
  time: number;
52
52
  closedPnl: string;
53
53
  fee: string;
54
+ /** Token the fee is denominated in. Spot buys typically pay fee in the base asset (e.g. "HYPE") rather than "USDC"; consumers must convert using `px` to get a USD value. */
55
+ feeToken: string;
54
56
  oid: number;
55
57
  crossed: boolean;
56
58
  };
@@ -245,6 +247,7 @@ export class WebSocketManager {
245
247
  time: fill.time,
246
248
  closedPnl: fill.closedPnl,
247
249
  fee: fill.fee,
250
+ feeToken: fill.feeToken,
248
251
  oid: fill.oid,
249
252
  crossed: fill.crossed,
250
253
  });
@@ -39,7 +39,7 @@ async function main() {
39
39
 
40
40
  const hourlyRate = parseFloat(ctx.funding);
41
41
  const annualizedRate = annualizeFundingRate(hourlyRate);
42
- const premium = parseFloat(ctx.premium);
42
+ const premium = parseFloat(ctx.premium ?? '0');
43
43
  const openInterest = parseFloat(ctx.openInterest);
44
44
  const markPx = parseFloat(ctx.markPx);
45
45
 
@@ -129,7 +129,7 @@ async function main() {
129
129
  console.log('\nResult');
130
130
  console.log('------');
131
131
 
132
- if (response.status === 'ok' && response.response) {
132
+ if (response.status === 'ok' && response.response && typeof response.response !== 'string') {
133
133
  const statuses = response.response.data.statuses;
134
134
  for (const status of statuses) {
135
135
  if (status.filled) {
@@ -49,11 +49,13 @@ async function main() {
49
49
 
50
50
  const response = await client.twapCancel(coin, twapId);
51
51
 
52
+ // SDK returns a success-only response shape; failures throw and are
53
+ // caught by the surrounding try/catch. `status` is the success literal.
52
54
  const status = response.response.data.status;
53
55
  if (typeof status === 'string' && status === 'success') {
54
56
  console.log(`\nTWAP order ${twapId} cancelled successfully.`);
55
- } else if (typeof status === 'object' && 'error' in status) {
56
- console.error(`\nFailed to cancel TWAP: ${status.error}`);
57
+ } else {
58
+ console.error(`\nUnexpected TWAP cancel response status: ${JSON.stringify(status)}`);
57
59
  process.exit(1);
58
60
  }
59
61
  } catch (error) {
@@ -123,17 +123,15 @@ async function main() {
123
123
  leverage,
124
124
  );
125
125
 
126
+ // SDK's TwapOrderSuccessResponse excludes the error variant — the SDK
127
+ // throws on failure and the surrounding try/catch handles that. So
128
+ // `status` only ever carries `{ running }` here.
126
129
  const status = response.response.data.status;
127
- if ('running' in status) {
128
- console.log(`TWAP order placed successfully!`);
129
- console.log(`TWAP ID: ${status.running.twapId}`);
130
- console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
131
- console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
132
- console.log(`To check status: openbroker twap-status`);
133
- } else if ('error' in status) {
134
- console.error(`TWAP order failed: ${status.error}`);
135
- process.exit(1);
136
- }
130
+ console.log(`TWAP order placed successfully!`);
131
+ console.log(`TWAP ID: ${status.running.twapId}`);
132
+ console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
133
+ console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
134
+ console.log(`To check status: openbroker twap-status`);
137
135
  } catch (error) {
138
136
  console.error('Error:', error instanceof Error ? error.message : error);
139
137
  process.exit(1);
@@ -1385,24 +1385,21 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1385
1385
  const midPrice = parseFloat(mids[coin]);
1386
1386
 
1387
1387
  const response = await client.twapOrder(coin, isBuy, size, durationMinutes, randomize, reduceOnly, leverage);
1388
+ // SDK's TwapOrderSuccessResponse excludes errors; they'd throw and
1389
+ // be caught below. Status is always `{ running }` on this branch.
1388
1390
  const status = response.response.data.status;
1389
1391
 
1390
- if ('running' in status) {
1391
- return json({
1392
- twapId: status.running.twapId,
1393
- coin,
1394
- side: isBuy ? 'buy' : 'sell',
1395
- size,
1396
- durationMinutes,
1397
- randomize,
1398
- reduceOnly,
1399
- estimatedNotional: midPrice ? midPrice * size : undefined,
1400
- midPrice: midPrice || undefined,
1401
- });
1402
- } else if ('error' in status) {
1403
- return error(status.error);
1404
- }
1405
- return error('Unexpected response');
1392
+ return json({
1393
+ twapId: status.running.twapId,
1394
+ coin,
1395
+ side: isBuy ? 'buy' : 'sell',
1396
+ size,
1397
+ durationMinutes,
1398
+ randomize,
1399
+ reduceOnly,
1400
+ estimatedNotional: midPrice ? midPrice * size : undefined,
1401
+ midPrice: midPrice || undefined,
1402
+ });
1406
1403
  } catch (err) {
1407
1404
  return error(err instanceof Error ? err.message : String(err));
1408
1405
  }
@@ -1428,14 +1425,13 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1428
1425
  try {
1429
1426
  const client = getClient();
1430
1427
  const response = await client.twapCancel(coin, twapId);
1428
+ // SDK returns the success-only response; failures throw.
1431
1429
  const status = response.response.data.status;
1432
1430
 
1433
1431
  if (typeof status === 'string' && status === 'success') {
1434
1432
  return json({ cancelled: true, coin, twapId });
1435
- } else if (typeof status === 'object' && 'error' in status) {
1436
- return error(status.error);
1437
1433
  }
1438
- return error('Unexpected response');
1434
+ return error(`Unexpected TWAP cancel status: ${JSON.stringify(status)}`);
1439
1435
  } catch (err) {
1440
1436
  return error(err instanceof Error ? err.message : String(err));
1441
1437
  }
@@ -387,8 +387,10 @@ Examples:
387
387
  return setupApiWallet();
388
388
  }
389
389
 
390
- // Options 1 & 2: Master wallet flow
391
- let privateKey: `0x${string}`;
390
+ // Options 1 & 2: Master wallet flow. Initialised in the branches below —
391
+ // the compiler can't prove the while-loop in option 2 assigns, so we use
392
+ // `| undefined` and narrow at use sites (`assertDefined`).
393
+ let privateKey: `0x${string}` | undefined;
392
394
 
393
395
  if (choice === '2') {
394
396
  // User has existing key
@@ -417,6 +419,10 @@ Examples:
417
419
  console.log('✅ New wallet created');
418
420
  }
419
421
 
422
+ if (!privateKey) {
423
+ throw new Error('Internal error: privateKey was not set by onboarding flow.');
424
+ }
425
+
420
426
  // Derive account from private key
421
427
  const account = privateKeyToAccount(privateKey);
422
428
  console.log(`\nWallet Address: ${account.address}\n`);