openbroker 1.0.72 → 1.0.75

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.72", "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.75", "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_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
 
@@ -127,8 +127,10 @@ HYPERLIQUID_NETWORK=mainnet
127
127
  ```bash
128
128
  openbroker account # Balance, equity, margin
129
129
  openbroker account --orders # Include open orders
130
+ openbroker account --address 0xabc... # Look up another account
130
131
  openbroker positions # Open positions with PnL
131
132
  openbroker positions --coin ETH # Specific coin
133
+ openbroker positions --address 0xabc... # Another account's positions
132
134
  ```
133
135
 
134
136
  ### Funding Rates
@@ -164,6 +166,7 @@ openbroker search --query ETH --type perp # ETH perps only
164
166
  openbroker spot # Show all spot markets
165
167
  openbroker spot --coin PURR # Show PURR market info
166
168
  openbroker spot --balances # Show your spot balances
169
+ openbroker spot --balances --address 0xabc... # Another account's spot balances
167
170
  openbroker spot --top 20 # Top 20 by volume
168
171
  ```
169
172
 
@@ -172,6 +175,7 @@ openbroker spot --top 20 # Top 20 by volume
172
175
  openbroker fills # Recent fills
173
176
  openbroker fills --coin ETH # ETH fills only
174
177
  openbroker fills --coin BTC --side buy --top 50
178
+ openbroker fills --address 0xabc... # Another account's fills
175
179
  ```
176
180
 
177
181
  ### Order History
@@ -181,17 +185,20 @@ openbroker orders --open # Currently open orders only
181
185
  openbroker orders --open --coin ETH # Open orders for a specific coin
182
186
  openbroker orders --coin ETH --status filled
183
187
  openbroker orders --top 50
188
+ openbroker orders --address 0xabc... --open # Another account's open orders
184
189
  ```
185
190
 
186
191
  ### Order Status
187
192
  ```bash
188
193
  openbroker order-status --oid 123456789 # Check specific order
189
194
  openbroker order-status --oid 0x1234... # By client order ID
195
+ openbroker order-status --oid 123456789 --address 0xabc... # On another account
190
196
  ```
191
197
 
192
198
  ### Fee Schedule
193
199
  ```bash
194
200
  openbroker fees # Fee tier, rates, and volume
201
+ openbroker fees --address 0xabc... # Another account's fees
195
202
  ```
196
203
 
197
204
  ### Candle Data (OHLCV)
@@ -521,11 +528,11 @@ To view bundled examples and their config schemas:
521
528
  openbroker auto examples # List examples with config fields
522
529
  ```
523
530
 
524
- Available examples: `dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`
531
+ Available examples: `dca`, `grid`, `funding-arb`, `mm-spread`, `mm-maker`, `price-alert`
525
532
 
526
533
  ### How Automations Work
527
534
 
528
- An automation is a `.ts` file that exports a default function. The function receives an `AutomationAPI` with the full Hyperliquid client, typed event subscriptions, persistent state, and a logger. The runtime polls Hyperliquid every 10s (configurable) and dispatches events when changes are detected.
535
+ An automation is a `.ts` file that exports a default function. The function receives an `AutomationAPI` with the full Hyperliquid client, typed event subscriptions, persistent state, and a logger. The runtime connects a WebSocket for real-time price and order events, with REST polling every 30s as a heartbeat for position/margin data. Use `--no-ws` to disable WebSocket and fall back to pure REST polling (every 10s).
529
536
 
530
537
  ### Writing an Automation
531
538
 
@@ -773,18 +780,66 @@ api.on('margin_warning', async ({ marginUsedPct, equity }) => {
773
780
  });
774
781
  ```
775
782
 
783
+ #### `order_update` — Real-time order lifecycle (WebSocket)
784
+ Fires instantly when any order changes status: `open`, `filled`, `canceled`, `triggered`, `rejected`, `marginCanceled`, `liquidatedCanceled`, `badAloPxRejected`, and 20+ other statuses. Requires WebSocket (enabled by default).
785
+
786
+ **Payload:** `{ coin: string, oid: number, side: 'buy' | 'sell', size: number, price: number, origSize: number, status: string, statusTimestamp: number }`
787
+
788
+ **Example:**
789
+ ```typescript
790
+ api.on('order_update', async ({ coin, oid, status, side, size, price }) => {
791
+ if (status === 'filled') {
792
+ api.log.info(`Order ${oid} filled: ${side} ${size} ${coin} @ $${price}`);
793
+ } else if (status === 'canceled' || status.includes('Rejected')) {
794
+ api.log.warn(`Order ${oid} ${status}: ${coin}`);
795
+ }
796
+ });
797
+ ```
798
+
799
+ #### `liquidation` — Liquidation alert (WebSocket only)
800
+ Fires when the account is liquidated. This event is **only available via WebSocket** — there is no REST polling equivalent.
801
+
802
+ **Payload:** `{ lid: number, liquidator: string, liquidatedUser: string, liquidatedNtlPos: number, liquidatedAccountValue: number }`
803
+
804
+ **Example:**
805
+ ```typescript
806
+ api.on('liquidation', async ({ liquidatedNtlPos, liquidatedAccountValue }) => {
807
+ await api.publish(
808
+ `LIQUIDATED: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`,
809
+ { name: 'liquidation-alert' },
810
+ );
811
+ });
812
+ ```
813
+
814
+ ### WebSocket Real-Time Data
815
+
816
+ Automations use **WebSocket by default** for real-time market and account events. The runtime subscribes to:
817
+ - **allMids** — price updates for all assets (drives `price_change` events in real-time)
818
+ - **orderUpdates** — order lifecycle events (drives `order_update` and `order_filled`)
819
+ - **userFills** — trade fill details with PnL and fees
820
+ - **userEvents** — liquidation alerts, funding payments, system cancellations
821
+
822
+ REST polling continues as a **heartbeat** (every 60s by default) for position/margin/funding events that aren't covered by WebSocket. If the WebSocket connection fails, the runtime falls back to full REST polling (every 10s) automatically.
823
+
824
+ To disable WebSocket (pure REST polling):
825
+ ```bash
826
+ openbroker auto run my-strategy.ts --no-ws
827
+ ```
828
+
776
829
  ### Choosing the Right Event — Quick Guide
777
830
 
778
831
  | Use case | Best event | Why |
779
832
  |----------|-----------|-----|
780
833
  | Alert when price crosses a fixed level | `tick` | Fires every poll — no minimum change threshold |
781
- | React to price momentum/volatility | `price_change` | Provides relative change data between polls |
834
+ | React to price momentum/volatility | `price_change` | Real-time via WebSocket, provides relative change data |
782
835
  | Funding rate strategy | `funding_update` | Gives annualized rate directly |
783
836
  | Auto TP/SL on new positions | `position_opened` | Fires exactly when a new position appears |
784
837
  | Log when positions close | `position_closed` | Fires when position disappears |
785
838
  | Track position scaling | `position_changed` | Fires on size changes only |
786
839
  | Risk management — PnL spikes | `pnl_threshold` | Only fires on large moves (≥5% of position value) |
787
840
  | Risk management — margin | `margin_warning` | Fires at 80%+ margin usage |
841
+ | React instantly to order fills/rejects | `order_update` | Real-time via WebSocket — sub-second latency |
842
+ | Liquidation alerts | `liquidation` | WebSocket only — no REST equivalent |
788
843
  | Periodic task (DCA, rebalance) | `api.every(ms, fn)` | Better than tick for longer intervals |
789
844
 
790
845
  ### Client Methods Available
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.72",
4
+ "version": "1.0.75",
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.72",
3
+ "version": "1.0.75",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -104,10 +104,11 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
104
104
  const scriptPath = exampleName ? resolveExamplePath(exampleName) : resolveScriptPath(scriptName!);
105
105
  const dryRun = args.dry === true;
106
106
  const verbose = args.verbose === true;
107
- const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : 10_000;
107
+ const useWebSocket = args['no-ws'] !== true;
108
+ const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : undefined;
108
109
  const id = args.id ? String(args.id) : undefined;
109
110
 
110
- if (isNaN(pollIntervalMs) || pollIntervalMs < 1000) {
111
+ if (pollIntervalMs !== undefined && (isNaN(pollIntervalMs) || pollIntervalMs < 1000)) {
111
112
  console.error('Error: --poll must be at least 1000ms');
112
113
  process.exit(1);
113
114
  }
@@ -118,6 +119,7 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
118
119
  dryRun,
119
120
  verbose,
120
121
  pollIntervalMs,
122
+ useWebSocket,
121
123
  initialState: Object.keys(initialState).length > 0 ? initialState : undefined,
122
124
  });
123
125
 
@@ -0,0 +1,96 @@
1
+ // Price Alert — Real-time price monitoring via WebSocket
2
+ // Showcases WebSocket-driven price_change and order_update events
3
+
4
+ import type { AutomationAPI, AutomationConfig } from '../types.js';
5
+
6
+ export const config: AutomationConfig = {
7
+ description: 'Real-time price alerts via WebSocket — log price moves and order updates',
8
+ fields: {
9
+ coin: { type: 'string', description: 'Asset to monitor', default: 'BTC' },
10
+ threshold: { type: 'number', description: 'Min price change % to alert on', default: 0.1 },
11
+ above: { type: 'number', description: 'Alert when price goes above this level (0 = disabled)', default: 0 },
12
+ below: { type: 'number', description: 'Alert when price goes below this level (0 = disabled)', default: 0 },
13
+ },
14
+ };
15
+
16
+ export default function priceAlert(api: AutomationAPI) {
17
+ const COIN = api.state.get<string>('coin', 'BTC')!;
18
+ const THRESHOLD = api.state.get<number>('threshold', 0.1)!;
19
+ const ABOVE = api.state.get<number>('above', 0)!;
20
+ const BELOW = api.state.get<number>('below', 0)!;
21
+
22
+ let alertCount = 0;
23
+ let lastAlertPrice = 0;
24
+ let aboveTriggered = false;
25
+ let belowTriggered = false;
26
+
27
+ api.onStart(() => {
28
+ api.log.info(`Monitoring ${COIN} via WebSocket`);
29
+ api.log.info(`Threshold: ${THRESHOLD}% change`);
30
+ if (ABOVE > 0) api.log.info(`Alert above: $${ABOVE}`);
31
+ if (BELOW > 0) api.log.info(`Alert below: $${BELOW}`);
32
+ });
33
+
34
+ // Real-time price changes via WebSocket
35
+ api.on('price_change', ({ coin, oldPrice, newPrice, changePct }) => {
36
+ if (coin !== COIN) return;
37
+
38
+ // Threshold alerts — fires when move exceeds configured %
39
+ if (Math.abs(changePct) >= THRESHOLD) {
40
+ const dir = changePct > 0 ? 'UP' : 'DOWN';
41
+ api.log.info(`${COIN} ${dir} ${changePct.toFixed(3)}%: $${oldPrice.toFixed(2)} -> $${newPrice.toFixed(2)}`);
42
+ alertCount++;
43
+ lastAlertPrice = newPrice;
44
+ }
45
+
46
+ // Level alerts — fires once when price crosses a level, resets when it crosses back
47
+ if (ABOVE > 0) {
48
+ if (newPrice >= ABOVE && !aboveTriggered) {
49
+ aboveTriggered = true;
50
+ api.log.info(`${COIN} ABOVE $${ABOVE}: now $${newPrice.toFixed(2)}`);
51
+ api.publish(`${COIN} broke above $${ABOVE} — now $${newPrice.toFixed(2)}`, { name: 'price-alert' });
52
+ } else if (newPrice < ABOVE) {
53
+ aboveTriggered = false;
54
+ }
55
+ }
56
+
57
+ if (BELOW > 0) {
58
+ if (newPrice <= BELOW && !belowTriggered) {
59
+ belowTriggered = true;
60
+ api.log.info(`${COIN} BELOW $${BELOW}: now $${newPrice.toFixed(2)}`);
61
+ api.publish(`${COIN} dropped below $${BELOW} — now $${newPrice.toFixed(2)}`, { name: 'price-alert' });
62
+ } else if (newPrice > BELOW) {
63
+ belowTriggered = false;
64
+ }
65
+ }
66
+ });
67
+
68
+ // Real-time order lifecycle via WebSocket
69
+ api.on('order_update', ({ coin, oid, side, size, price, status }) => {
70
+ if (status === 'filled') {
71
+ api.log.info(`ORDER FILLED: ${side.toUpperCase()} ${size} ${coin} @ $${price.toFixed(2)} (oid: ${oid})`);
72
+ } else if (status === 'canceled' || status.includes('Canceled') || status.includes('Rejected')) {
73
+ api.log.warn(`ORDER ${status.toUpperCase()}: ${side} ${size} ${coin} @ $${price.toFixed(2)} (oid: ${oid})`);
74
+ }
75
+ });
76
+
77
+ // Liquidation alerts via WebSocket
78
+ api.on('liquidation', ({ liquidatedNtlPos, liquidatedAccountValue }) => {
79
+ api.log.error(`LIQUIDATION: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`);
80
+ api.publish(
81
+ `LIQUIDATED: $${liquidatedNtlPos.toFixed(2)} notional, account value: $${liquidatedAccountValue.toFixed(2)}`,
82
+ { name: 'liquidation-alert' },
83
+ );
84
+ });
85
+
86
+ // Periodic summary via REST heartbeat
87
+ api.on('tick', ({ pollCount }) => {
88
+ if (pollCount % 10 === 0 && alertCount > 0) {
89
+ api.log.info(`Summary: ${alertCount} alerts fired, last price: $${lastAlertPrice.toFixed(2)}`);
90
+ }
91
+ });
92
+
93
+ api.onStop(() => {
94
+ api.log.info(`Stopped. Total alerts: ${alertCount}`);
95
+ });
96
+ }
@@ -1,4 +1,5 @@
1
1
  // Automation Runtime — loads scripts, polls market data, dispatches events
2
+ // Supports real-time WebSocket feeds with REST polling as fallback heartbeat
2
3
 
3
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
5
  import path from 'path';
@@ -9,6 +10,7 @@ import {
9
10
  roundPrice, roundSize, sleep, normalizeCoin,
10
11
  formatUsd, formatPercent, annualizeFundingRate,
11
12
  } from '../core/utils.js';
13
+ import { WebSocketManager } from '../core/ws.js';
12
14
  import { AutomationEventBus } from './events.js';
13
15
  import { loadAutomation } from './loader.js';
14
16
  import { registerAutomation, unregisterAutomation, markAutomationError, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
@@ -233,6 +235,13 @@ export interface RuntimeOptions {
233
235
  hooksToken?: string;
234
236
  /** Pre-seed state before the factory function runs (e.g. from --set key=value) */
235
237
  initialState?: Record<string, unknown>;
238
+ /**
239
+ * Enable WebSocket for real-time events (allMids, orderUpdates, userFills, userEvents).
240
+ * When enabled, REST polling interval is relaxed to a heartbeat (default 60s).
241
+ * Falls back gracefully to polling if WebSocket connection fails.
242
+ * @default true
243
+ */
244
+ useWebSocket?: boolean;
236
245
  }
237
246
 
238
247
  /** Registry of all running automations */
@@ -254,12 +263,16 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
254
263
  scriptPath,
255
264
  dryRun = false,
256
265
  verbose = false,
257
- pollIntervalMs = 10_000,
258
266
  gatewayPort,
259
267
  hooksToken,
260
268
  initialState,
269
+ useWebSocket = true,
261
270
  } = options;
262
271
 
272
+ // When WebSocket is enabled, REST poll becomes a heartbeat (30s default)
273
+ // When disabled, use the original 10s polling interval
274
+ const pollIntervalMs = options.pollIntervalMs ?? (useWebSocket ? 30_000 : 10_000);
275
+
263
276
  const id = options.id || path.basename(scriptPath, '.ts');
264
277
 
265
278
  if (registry.has(id)) {
@@ -317,7 +330,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
317
330
  }
318
331
  }
319
332
 
320
- // Polling state
333
+ // Polling state (declared early so WebSocket handlers can reference)
321
334
  let previousSnapshot: AutomationSnapshot | null = null;
322
335
  let pollCount = 0;
323
336
  let eventsEmitted = 0;
@@ -333,6 +346,110 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
333
346
  }
334
347
  }
335
348
 
349
+ // ── WebSocket setup ─────────────────────────────────────────────
350
+ let ws: WebSocketManager | null = null;
351
+ let wsConnected = false;
352
+ // Track latest prices from WebSocket for real-time price_change events
353
+ let wsPrices = new Map<string, number>();
354
+
355
+ if (useWebSocket) {
356
+ try {
357
+ ws = new WebSocketManager(verbose);
358
+
359
+ // Wire WebSocket events to the automation event bus
360
+ ws.on('allMids', ({ mids }) => {
361
+ const now = Date.now();
362
+ for (const [coin, mid] of Object.entries(mids)) {
363
+ const newPrice = parseFloat(mid);
364
+ if (isNaN(newPrice) || newPrice === 0) continue;
365
+ const oldPrice = wsPrices.get(coin);
366
+ wsPrices.set(coin, newPrice);
367
+
368
+ if (oldPrice !== undefined && oldPrice !== 0 && eventBus.has('price_change')) {
369
+ const changePct = ((newPrice - oldPrice) / oldPrice) * 100;
370
+ if (Math.abs(changePct) >= 0.01) {
371
+ eventBus.emit('price_change', { coin, oldPrice, newPrice, changePct })
372
+ .then(errors => { if (errors.length) handleErrors(errors); });
373
+ eventsEmitted++;
374
+ }
375
+ }
376
+ }
377
+ });
378
+
379
+ ws.on('orderUpdate', (update) => {
380
+ if (eventBus.has('order_update')) {
381
+ eventBus.emit('order_update', {
382
+ coin: update.order.coin,
383
+ oid: update.order.oid,
384
+ side: update.order.side === 'B' ? 'buy' : 'sell',
385
+ size: parseFloat(update.order.sz),
386
+ price: parseFloat(update.order.limitPx),
387
+ origSize: parseFloat(update.order.origSz),
388
+ status: update.status,
389
+ statusTimestamp: update.statusTimestamp,
390
+ }).then(errors => { if (errors.length) handleErrors(errors); });
391
+ eventsEmitted++;
392
+ }
393
+
394
+ // Also emit order_filled for backward compatibility
395
+ if (update.status === 'filled' && eventBus.has('order_filled')) {
396
+ eventBus.emit('order_filled', {
397
+ coin: update.order.coin,
398
+ oid: update.order.oid,
399
+ side: update.order.side === 'B' ? 'buy' : 'sell',
400
+ size: parseFloat(update.order.sz),
401
+ price: parseFloat(update.order.limitPx),
402
+ }).then(errors => { if (errors.length) handleErrors(errors); });
403
+ eventsEmitted++;
404
+ }
405
+ });
406
+
407
+ ws.on('userFill', (fill) => {
408
+ // userFill events are already covered by order_update with status=filled
409
+ // But this provides the realized PnL and fee data that order_update doesn't have
410
+ log.debug(`Fill: ${fill.side === 'B' ? 'BUY' : 'SELL'} ${fill.sz} ${fill.coin} @ ${fill.px} (PnL: ${fill.closedPnl})`);
411
+ });
412
+
413
+ ws.on('userEvent', (event) => {
414
+ // Handle liquidation events — only available through WebSocket
415
+ if ('liquidation' in event && eventBus.has('liquidation')) {
416
+ const liq = event.liquidation;
417
+ eventBus.emit('liquidation', {
418
+ lid: liq.lid,
419
+ liquidator: liq.liquidator,
420
+ liquidatedUser: liq.liquidated_user,
421
+ liquidatedNtlPos: parseFloat(liq.liquidated_ntl_pos),
422
+ liquidatedAccountValue: parseFloat(liq.liquidated_account_value),
423
+ }).then(errors => { if (errors.length) handleErrors(errors); });
424
+ eventsEmitted++;
425
+ }
426
+ });
427
+
428
+ ws.on('error', ({ error }) => {
429
+ log.warn(`WebSocket error: ${error.message}`);
430
+ });
431
+
432
+ ws.on('disconnected', () => {
433
+ wsConnected = false;
434
+ log.warn('WebSocket disconnected — falling back to REST polling');
435
+ });
436
+
437
+ ws.on('connected', () => {
438
+ wsConnected = true;
439
+ log.info('WebSocket connected — real-time events active');
440
+ });
441
+
442
+ // Connect and subscribe
443
+ const userAddress = rawClient.address as `0x${string}`;
444
+ await ws.subscribeAll(userAddress);
445
+ log.info('WebSocket subscriptions active (allMids, orderUpdates, userFills, userEvents)');
446
+ } catch (err) {
447
+ log.warn(`WebSocket setup failed: ${err instanceof Error ? err.message : String(err)} — using REST polling only`);
448
+ ws = null;
449
+ wsConnected = false;
450
+ }
451
+ }
452
+
336
453
  async function poll() {
337
454
  if (isPolling || stopped) return;
338
455
  isPolling = true;
@@ -348,8 +465,8 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
348
465
  eventsEmitted++;
349
466
 
350
467
  if (previousSnapshot) {
351
- // Price changes
352
- if (eventBus.has('price_change')) {
468
+ // Price changes (skip when WebSocket is handling real-time prices)
469
+ if (eventBus.has('price_change') && !wsConnected) {
353
470
  for (const [coin, newPrice] of snapshot.prices) {
354
471
  const oldPrice = previousSnapshot.prices.get(coin);
355
472
  if (oldPrice === undefined || oldPrice === 0) continue;
@@ -485,7 +602,8 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
485
602
  }
486
603
 
487
604
  // Start polling
488
- log.info(`Started (poll every ${pollIntervalMs / 1000}s, dry=${dryRun})`);
605
+ const wsLabel = wsConnected ? ', ws=on' : (useWebSocket ? ', ws=failed' : '');
606
+ log.info(`Started (poll every ${pollIntervalMs / 1000}s, dry=${dryRun}${wsLabel})`);
489
607
  const timer = setInterval(poll, pollIntervalMs);
490
608
 
491
609
  // Initial poll to seed state
@@ -497,6 +615,13 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
497
615
  stopped = true;
498
616
  clearInterval(timer);
499
617
 
618
+ // Close WebSocket
619
+ if (ws) {
620
+ ws.removeAllListeners();
621
+ await ws.close();
622
+ ws = null;
623
+ }
624
+
500
625
  for (const hook of stopHooks) {
501
626
  try { await hook(); } catch (err) {
502
627
  log.error(`onStop hook error: ${err instanceof Error ? err.message : String(err)}`);
@@ -33,7 +33,9 @@ export type AutomationEventType =
33
33
  | 'position_changed'
34
34
  | 'pnl_threshold'
35
35
  | 'margin_warning'
36
- | 'order_filled';
36
+ | 'order_filled'
37
+ | 'order_update'
38
+ | 'liquidation';
37
39
 
38
40
  export interface AutomationEventPayloads {
39
41
  tick: { timestamp: number; pollCount: number };
@@ -45,6 +47,25 @@ export interface AutomationEventPayloads {
45
47
  pnl_threshold: { coin: string; unrealizedPnl: number; changePct: number; positionValue: number };
46
48
  margin_warning: { marginUsedPct: number; equity: number; marginUsed: number };
47
49
  order_filled: { coin: string; oid: number; side: 'buy' | 'sell'; size: number; price: number };
50
+ /** Real-time order lifecycle event via WebSocket (filled, canceled, rejected, triggered, etc.) */
51
+ order_update: {
52
+ coin: string;
53
+ oid: number;
54
+ side: 'buy' | 'sell';
55
+ size: number;
56
+ price: number;
57
+ origSize: number;
58
+ status: string;
59
+ statusTimestamp: number;
60
+ };
61
+ /** Liquidation event via WebSocket — only source for liquidation alerts */
62
+ liquidation: {
63
+ lid: number;
64
+ liquidator: string;
65
+ liquidatedUser: string;
66
+ liquidatedNtlPos: number;
67
+ liquidatedAccountValue: number;
68
+ };
48
69
  }
49
70
 
50
71
  export type AutomationEventHandler<E extends AutomationEventType> =
@@ -0,0 +1,283 @@
1
+ // WebSocket Manager for Hyperliquid real-time data
2
+ // Wraps @nktkas/hyperliquid SubscriptionClient with event-driven API
3
+
4
+ import { WebSocketTransport, SubscriptionClient } from '@nktkas/hyperliquid';
5
+ import type { ISubscription } from '@nktkas/hyperliquid';
6
+ import type {
7
+ AllMidsWsEvent,
8
+ OrderUpdatesWsEvent,
9
+ UserFillsWsEvent,
10
+ UserEventsWsEvent,
11
+ } from '@nktkas/hyperliquid';
12
+ import { isMainnet } from './config.js';
13
+
14
+ // ── Event types ────────────────────────────────────────────────────
15
+
16
+ export interface WsEventMap {
17
+ /** Mid prices for all assets updated */
18
+ allMids: { mids: Record<string, string> };
19
+ /** Order status changed (filled, canceled, rejected, etc.) */
20
+ orderUpdate: {
21
+ order: {
22
+ coin: string;
23
+ side: 'B' | 'A';
24
+ limitPx: string;
25
+ sz: string;
26
+ oid: number;
27
+ timestamp: number;
28
+ origSz: string;
29
+ cloid?: string;
30
+ reduceOnly?: boolean;
31
+ };
32
+ status: string;
33
+ statusTimestamp: number;
34
+ };
35
+ /** Trade fill received */
36
+ userFill: {
37
+ coin: string;
38
+ px: string;
39
+ sz: string;
40
+ side: 'B' | 'A';
41
+ time: number;
42
+ closedPnl: string;
43
+ fee: string;
44
+ oid: number;
45
+ crossed: boolean;
46
+ };
47
+ /** User event (fills, funding, liquidation, non-user cancels) */
48
+ userEvent: UserEventsWsEvent;
49
+ /** WebSocket connected */
50
+ connected: undefined;
51
+ /** WebSocket disconnected */
52
+ disconnected: { reason?: string };
53
+ /** WebSocket error */
54
+ error: { error: Error };
55
+ }
56
+
57
+ export type WsEventType = keyof WsEventMap;
58
+ export type WsEventHandler<E extends WsEventType> = (data: WsEventMap[E]) => void;
59
+
60
+ // ── Manager ────────────────────────────────────────────────────────
61
+
62
+ export class WebSocketManager {
63
+ private transport: WebSocketTransport | null = null;
64
+ private client: SubscriptionClient | null = null;
65
+ private subscriptions: ISubscription[] = [];
66
+ private handlers = new Map<WsEventType, Set<Function>>();
67
+ private _connected = false;
68
+ private verbose: boolean;
69
+
70
+ constructor(verbose = false) {
71
+ this.verbose = verbose;
72
+ }
73
+
74
+ private log(...args: unknown[]) {
75
+ if (this.verbose) console.log('[WS]', ...args);
76
+ }
77
+
78
+ get connected(): boolean {
79
+ return this._connected;
80
+ }
81
+
82
+ // ── Connection management ──────────────────────────────────────
83
+
84
+ async connect(): Promise<void> {
85
+ if (this.transport) return; // already connected
86
+
87
+ this.transport = new WebSocketTransport({
88
+ isTestnet: !isMainnet(),
89
+ resubscribe: true, // auto-resubscribe on reconnect
90
+ });
91
+
92
+ this.client = new SubscriptionClient({ transport: this.transport });
93
+
94
+ await this.transport.ready();
95
+ this._connected = true;
96
+ this.emit('connected', undefined);
97
+ this.log('Connected to', isMainnet() ? 'mainnet' : 'testnet');
98
+ }
99
+
100
+ async close(): Promise<void> {
101
+ for (const sub of this.subscriptions) {
102
+ try { await sub.unsubscribe(); } catch { /* ignore */ }
103
+ }
104
+ this.subscriptions = [];
105
+
106
+ if (this.transport) {
107
+ try { await this.transport.close(); } catch { /* ignore */ }
108
+ this.transport = null;
109
+ this.client = null;
110
+ }
111
+
112
+ this._connected = false;
113
+ this.emit('disconnected', { reason: 'manual close' });
114
+ this.log('Closed');
115
+ }
116
+
117
+ // ── Event system ───────────────────────────────────────────────
118
+
119
+ on<E extends WsEventType>(event: E, handler: WsEventHandler<E>): void {
120
+ if (!this.handlers.has(event)) {
121
+ this.handlers.set(event, new Set());
122
+ }
123
+ this.handlers.get(event)!.add(handler);
124
+ }
125
+
126
+ off<E extends WsEventType>(event: E, handler: WsEventHandler<E>): void {
127
+ this.handlers.get(event)?.delete(handler);
128
+ }
129
+
130
+ private emit<E extends WsEventType>(event: E, data: WsEventMap[E]): void {
131
+ const set = this.handlers.get(event);
132
+ if (!set) return;
133
+ for (const handler of set) {
134
+ try {
135
+ handler(data);
136
+ } catch (err) {
137
+ this.log('Handler error:', err instanceof Error ? err.message : String(err));
138
+ this.emit('error' as E, { error: err instanceof Error ? err : new Error(String(err)) } as WsEventMap[E]);
139
+ }
140
+ }
141
+ }
142
+
143
+ removeAllListeners(): void {
144
+ this.handlers.clear();
145
+ }
146
+
147
+ // ── Subscription helpers ───────────────────────────────────────
148
+
149
+ private ensureClient(): SubscriptionClient {
150
+ if (!this.client) throw new Error('WebSocket not connected. Call connect() first.');
151
+ return this.client;
152
+ }
153
+
154
+ private trackSub(sub: ISubscription): ISubscription {
155
+ this.subscriptions.push(sub);
156
+ sub.failureSignal.addEventListener('abort', () => {
157
+ this.log('Subscription failed, removing from tracked list');
158
+ const idx = this.subscriptions.indexOf(sub);
159
+ if (idx >= 0) this.subscriptions.splice(idx, 1);
160
+ });
161
+ return sub;
162
+ }
163
+
164
+ // ── Market data subscriptions ──────────────────────────────────
165
+
166
+ /**
167
+ * Subscribe to mid prices for all assets. Fires on every price update.
168
+ */
169
+ async subscribeAllMids(): Promise<ISubscription> {
170
+ const client = this.ensureClient();
171
+ const sub = await client.allMids((data: AllMidsWsEvent) => {
172
+ this.emit('allMids', { mids: data.mids });
173
+ });
174
+ return this.trackSub(sub);
175
+ }
176
+
177
+ // ── User subscriptions ────────────────────────────────────────
178
+
179
+ /**
180
+ * Subscribe to order lifecycle events (fill, cancel, reject, etc.).
181
+ * This is the most important subscription for trading automations.
182
+ */
183
+ async subscribeOrderUpdates(user: `0x${string}`): Promise<ISubscription> {
184
+ const client = this.ensureClient();
185
+ const sub = await client.orderUpdates({ user }, (data: OrderUpdatesWsEvent) => {
186
+ for (const update of data) {
187
+ this.emit('orderUpdate', {
188
+ order: {
189
+ coin: update.order.coin,
190
+ side: update.order.side,
191
+ limitPx: update.order.limitPx,
192
+ sz: update.order.sz,
193
+ oid: update.order.oid,
194
+ timestamp: update.order.timestamp,
195
+ origSz: update.order.origSz,
196
+ cloid: update.order.cloid,
197
+ reduceOnly: update.order.reduceOnly,
198
+ },
199
+ status: update.status,
200
+ statusTimestamp: update.statusTimestamp,
201
+ });
202
+ }
203
+ });
204
+ return this.trackSub(sub);
205
+ }
206
+
207
+ /**
208
+ * Subscribe to trade fills for a user.
209
+ */
210
+ async subscribeUserFills(user: `0x${string}`): Promise<ISubscription> {
211
+ const client = this.ensureClient();
212
+ const sub = await client.userFills({ user }, (data: UserFillsWsEvent) => {
213
+ if (data.isSnapshot) return; // skip initial snapshot
214
+ for (const fill of data.fills) {
215
+ this.emit('userFill', {
216
+ coin: fill.coin,
217
+ px: fill.px,
218
+ sz: fill.sz,
219
+ side: fill.side,
220
+ time: fill.time,
221
+ closedPnl: fill.closedPnl,
222
+ fee: fill.fee,
223
+ oid: fill.oid,
224
+ crossed: fill.crossed,
225
+ });
226
+ }
227
+ });
228
+ return this.trackSub(sub);
229
+ }
230
+
231
+ /**
232
+ * Subscribe to all user events (fills, funding, liquidations, non-user cancels).
233
+ * This is the only way to get liquidation alerts.
234
+ */
235
+ async subscribeUserEvents(user: `0x${string}`): Promise<ISubscription> {
236
+ const client = this.ensureClient();
237
+ const sub = await client.userEvents({ user }, (data: UserEventsWsEvent) => {
238
+ this.emit('userEvent', data);
239
+ });
240
+ return this.trackSub(sub);
241
+ }
242
+
243
+ // ── Convenience: subscribe to all relevant feeds for an automation ──
244
+
245
+ /**
246
+ * Start all subscriptions needed for the automation runtime:
247
+ * - allMids (price feed)
248
+ * - orderUpdates (order lifecycle)
249
+ * - userFills (trade fills)
250
+ * - userEvents (liquidations, funding payments, system cancels)
251
+ */
252
+ async subscribeAll(user: `0x${string}`): Promise<void> {
253
+ await this.connect();
254
+ this.log('Subscribing to all feeds for', user);
255
+
256
+ await Promise.all([
257
+ this.subscribeAllMids(),
258
+ this.subscribeOrderUpdates(user),
259
+ this.subscribeUserFills(user),
260
+ this.subscribeUserEvents(user),
261
+ ]);
262
+
263
+ this.log('All subscriptions active');
264
+ }
265
+ }
266
+
267
+ // ── Singleton ──────────────────────────────────────────────────────
268
+
269
+ let wsInstance: WebSocketManager | null = null;
270
+
271
+ export function getWebSocket(verbose = false): WebSocketManager {
272
+ if (!wsInstance) {
273
+ wsInstance = new WebSocketManager(verbose);
274
+ }
275
+ return wsInstance;
276
+ }
277
+
278
+ export function resetWebSocket(): void {
279
+ if (wsInstance) {
280
+ wsInstance.close().catch(() => {});
281
+ wsInstance = null;
282
+ }
283
+ }
@@ -7,16 +7,20 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
7
7
  async function main() {
8
8
  const args = parseArgs(process.argv.slice(2));
9
9
  const jsonOutput = args.json as boolean;
10
+ const targetAddress = args.address as string | undefined;
10
11
  const client = getClient();
11
12
 
12
13
  if (args.verbose) {
13
14
  client.verbose = true;
14
15
  }
15
16
 
16
- const accountMode = await client.getAccountMode();
17
+ const lookupAddress = targetAddress?.toLowerCase() ?? client.address;
18
+ const isOtherAccount = !!targetAddress;
19
+
20
+ const accountMode = await client.getAccountMode(isOtherAccount ? lookupAddress : undefined);
17
21
 
18
22
  try {
19
- const state = await client.getUserStateAll();
23
+ const state = await client.getUserStateAll(isOtherAccount ? lookupAddress : undefined);
20
24
 
21
25
  const margin = state.crossMarginSummary;
22
26
  const accountValue = parseFloat(margin.accountValue);
@@ -48,12 +52,19 @@ async function main() {
48
52
 
49
53
  const totalPnl = positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
50
54
 
55
+ // Fetch spot balances
56
+ const userParam = isOtherAccount ? lookupAddress : undefined;
57
+ const spotState = await client.getSpotBalances(userParam);
58
+ const spotBalances = (spotState?.balances ?? []).filter(b => parseFloat(b.total) > 0);
59
+
51
60
  // JSON output
52
61
  if (jsonOutput) {
53
62
  const result: Record<string, unknown> = {
54
- address: client.address,
55
- signingWallet: client.walletAddress,
56
- walletType: client.isApiWallet ? 'api' : 'main',
63
+ address: lookupAddress,
64
+ ...(isOtherAccount ? {} : {
65
+ signingWallet: client.walletAddress,
66
+ walletType: client.isApiWallet ? 'api' : 'main',
67
+ }),
57
68
  accountMode,
58
69
  equity: accountValue,
59
70
  totalNotional,
@@ -62,10 +73,16 @@ async function main() {
62
73
  marginRatio: totalMarginUsed > 0 && accountValue > 0 ? totalMarginUsed / accountValue : 0,
63
74
  totalUnrealizedPnl: totalPnl,
64
75
  positions,
76
+ spotBalances: spotBalances.map(b => ({
77
+ coin: b.coin,
78
+ total: b.total,
79
+ hold: b.hold,
80
+ entryNtl: b.entryNtl,
81
+ })),
65
82
  };
66
83
 
67
84
  if (args.orders) {
68
- const orders = await client.getOpenOrders();
85
+ const orders = await client.getOpenOrders(isOtherAccount ? lookupAddress : undefined);
69
86
  result.openOrders = orders.map(o => ({
70
87
  coin: o.coin,
71
88
  oid: o.oid,
@@ -85,11 +102,17 @@ async function main() {
85
102
  console.log('Open Broker - Account Info');
86
103
  console.log('==========================\n');
87
104
 
88
- console.log('Wallet Configuration');
89
- console.log('--------------------');
90
- console.log(`Trading Account: ${client.address}`);
91
- console.log(`Signing Wallet: ${client.walletAddress}`);
92
- console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
105
+ if (isOtherAccount) {
106
+ console.log('Lookup Address');
107
+ console.log('--------------------');
108
+ console.log(`Address: ${lookupAddress}`);
109
+ } else {
110
+ console.log('Wallet Configuration');
111
+ console.log('--------------------');
112
+ console.log(`Trading Account: ${client.address}`);
113
+ console.log(`Signing Wallet: ${client.walletAddress}`);
114
+ console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
115
+ }
93
116
 
94
117
  const modeLabel: Record<string, string> = {
95
118
  standard: 'Standard (separate balances per dex)',
@@ -99,22 +122,24 @@ async function main() {
99
122
  };
100
123
  console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
101
124
 
102
- // Check builder fee approval
103
- const builderApproval = await client.getMaxBuilderFee();
104
- console.log(`Builder Address: ${client.builderAddress}`);
105
- console.log(`Builder Fee: ${client.builderFeeBps} bps`);
106
- if (builderApproval) {
107
- console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
108
- } else {
109
- console.log(`Builder Approved: ❌ No`);
110
- console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
111
- }
125
+ if (!isOtherAccount) {
126
+ // Check builder fee approval
127
+ const builderApproval = await client.getMaxBuilderFee();
128
+ console.log(`Builder Address: ${client.builderAddress}`);
129
+ console.log(`Builder Fee: ${client.builderFeeBps} bps`);
130
+ if (builderApproval) {
131
+ console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
132
+ } else {
133
+ console.log(`Builder Approved: No`);
134
+ console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
135
+ }
112
136
 
113
- // Warn if API wallet setup looks misconfigured
114
- if (!client.isApiWallet && accountValue === 0 && positions.length === 0) {
115
- console.log('\n⚠️ No positions and $0 equity.');
116
- console.log(' If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
117
- console.log(' in ~/.openbroker/.env to the master account address (the wallet that holds funds).');
137
+ // Warn if API wallet setup looks misconfigured
138
+ if (!client.isApiWallet && accountValue === 0 && positions.length === 0) {
139
+ console.log('\n⚠️ No positions and $0 equity.');
140
+ console.log(' If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
141
+ console.log(' in ~/.openbroker/.env to the master account address (the wallet that holds funds).');
142
+ }
118
143
  }
119
144
  console.log('');
120
145
 
@@ -152,12 +177,29 @@ async function main() {
152
177
  console.log(`Total Unrealized PnL: ${formatUsd(totalPnl)}`);
153
178
  }
154
179
 
180
+ // Show spot balances
181
+ if (spotBalances.length > 0) {
182
+ console.log('\nSpot Balances');
183
+ console.log('-------------');
184
+ console.log('Token | Total | Hold | Entry Value');
185
+ console.log('-------------|--------------------|--------------------|------------');
186
+
187
+ for (const b of spotBalances) {
188
+ const total = parseFloat(b.total);
189
+ const hold = parseFloat(b.hold);
190
+ const entry = parseFloat(b.entryNtl);
191
+ console.log(
192
+ `${b.coin.padEnd(12)} | ${total.toFixed(6).padStart(18)} | ${hold.toFixed(6).padStart(18)} | ${formatUsd(entry)}`
193
+ );
194
+ }
195
+ }
196
+
155
197
  // Show open orders if requested
156
198
  if (args.orders) {
157
199
  console.log('\nOpen Orders');
158
200
  console.log('-----------');
159
201
 
160
- const orders = await client.getOpenOrders();
202
+ const orders = await client.getOpenOrders(isOtherAccount ? lookupAddress : undefined);
161
203
  if (orders.length === 0) {
162
204
  console.log('No open orders');
163
205
  } else {
@@ -24,13 +24,19 @@ async function main() {
24
24
  process.exit(0);
25
25
  }
26
26
 
27
+ const targetAddress = args.address as string | undefined;
27
28
  const client = getClient();
29
+ const lookupAddress = targetAddress?.toLowerCase();
28
30
 
29
31
  console.log('Open Broker - Fee Schedule');
30
32
  console.log('=========================\n');
31
33
 
34
+ if (targetAddress) {
35
+ console.log(`Lookup: ${lookupAddress}\n`);
36
+ }
37
+
32
38
  try {
33
- const fees = await client.getUserFees();
39
+ const fees = await client.getUserFees(lookupAddress);
34
40
 
35
41
  // Fee rates
36
42
  console.log('Fee Rates');
@@ -9,15 +9,17 @@ function printUsage() {
9
9
  Usage: openbroker fills [options]
10
10
 
11
11
  Options:
12
- --coin <symbol> Filter by coin (e.g. ETH, BTC)
13
- --side <buy|sell> Filter by side
14
- --top <n> Show last N fills (default: 20)
15
- --help, -h Show this help
12
+ --coin <symbol> Filter by coin (e.g. ETH, BTC)
13
+ --side <buy|sell> Filter by side
14
+ --top <n> Show last N fills (default: 20)
15
+ --address <0x...> Look up another account's fills
16
+ --help, -h Show this help
16
17
 
17
18
  Examples:
18
19
  openbroker fills
19
20
  openbroker fills --coin ETH
20
21
  openbroker fills --coin BTC --side buy --top 50
22
+ openbroker fills --address 0xabc... --coin ETH
21
23
  `);
22
24
  }
23
25
 
@@ -33,10 +35,13 @@ async function main() {
33
35
  const filterSide = args.side as string | undefined;
34
36
  const top = parseInt(args.top as string) || 20;
35
37
  const jsonOutput = args.json as boolean;
38
+ const targetAddress = args.address as string | undefined;
36
39
  const client = getClient();
37
40
 
41
+ const lookupAddress = targetAddress?.toLowerCase();
42
+
38
43
  try {
39
- let fills = await client.getUserFills();
44
+ let fills = await client.getUserFills(lookupAddress);
40
45
 
41
46
  if (filterCoin) {
42
47
  fills = fills.filter(f => f.coin === normalizeCoin(filterCoin));
@@ -66,6 +71,10 @@ async function main() {
66
71
  console.log('Open Broker - Trade Fills');
67
72
  console.log('========================\n');
68
73
 
74
+ if (targetAddress) {
75
+ console.log(`Lookup: ${lookupAddress}\n`);
76
+ }
77
+
69
78
  if (fills.length === 0) {
70
79
  console.log('No fills found');
71
80
  return;
@@ -9,12 +9,14 @@ function printUsage() {
9
9
  Usage: openbroker order-status --oid <order-id>
10
10
 
11
11
  Options:
12
- --oid <id> Order ID (number) or client order ID (hex string) — required
13
- --help, -h Show this help
12
+ --oid <id> Order ID (number) or client order ID (hex string) — required
13
+ --address <0x...> Look up order on another account
14
+ --help, -h Show this help
14
15
 
15
16
  Examples:
16
17
  openbroker order-status --oid 123456789
17
18
  openbroker order-status --oid 0x1234abcd...
19
+ openbroker order-status --oid 123456789 --address 0xabc...
18
20
  `);
19
21
  }
20
22
 
@@ -34,13 +36,15 @@ async function main() {
34
36
  }
35
37
 
36
38
  const oid = oidArg.startsWith('0x') ? oidArg : parseInt(oidArg);
39
+ const targetAddress = args.address as string | undefined;
37
40
  const client = getClient();
41
+ const lookupAddress = targetAddress?.toLowerCase();
38
42
 
39
43
  console.log('Open Broker - Order Status');
40
44
  console.log('=========================\n');
41
45
 
42
46
  try {
43
- const result = await client.getOrderStatus(oid);
47
+ const result = await client.getOrderStatus(oid, lookupAddress);
44
48
 
45
49
  if (result.status === 'unknownOid') {
46
50
  console.log(`Order ${oidArg} not found`);
@@ -13,6 +13,7 @@ Options:
13
13
  --status <status> Filter by status (filled, canceled, open, triggered, rejected, etc.)
14
14
  --open Show only currently open orders
15
15
  --top <n> Show last N orders (default: 20)
16
+ --address <0x...> Look up another account's orders
16
17
  --help, -h Show this help
17
18
 
18
19
  Examples:
@@ -21,6 +22,7 @@ Examples:
21
22
  openbroker orders --open --coin ETH
22
23
  openbroker orders --coin ETH --status filled
23
24
  openbroker orders --top 50
25
+ openbroker orders --address 0xabc... --open
24
26
  `);
25
27
  }
26
28
 
@@ -37,12 +39,15 @@ async function main() {
37
39
  const openOnly = args.open as boolean;
38
40
  const top = parseInt(args.top as string) || 20;
39
41
  const jsonOutput = args.json as boolean;
42
+ const targetAddress = args.address as string | undefined;
40
43
  const client = getClient();
41
44
 
45
+ const lookupAddress = targetAddress?.toLowerCase();
46
+
42
47
  try {
43
48
  if (openOnly) {
44
49
  // Use the dedicated open orders endpoint
45
- let openOrders = await client.getOpenOrders();
50
+ let openOrders = await client.getOpenOrders(lookupAddress);
46
51
 
47
52
  if (filterCoin) {
48
53
  openOrders = openOrders.filter(o => o.coin === normalizeCoin(filterCoin));
@@ -69,6 +74,10 @@ async function main() {
69
74
  console.log('Open Broker - Open Orders');
70
75
  console.log('=========================\n');
71
76
 
77
+ if (targetAddress) {
78
+ console.log(`Lookup: ${lookupAddress}\n`);
79
+ }
80
+
72
81
  if (openOrders.length === 0) {
73
82
  console.log('No open orders found');
74
83
  return;
@@ -106,7 +115,7 @@ async function main() {
106
115
  return;
107
116
  }
108
117
 
109
- let orders = await client.getHistoricalOrders();
118
+ let orders = await client.getHistoricalOrders(lookupAddress);
110
119
 
111
120
  if (filterCoin) {
112
121
  orders = orders.filter(o => o.order.coin === normalizeCoin(filterCoin));
@@ -137,6 +146,10 @@ async function main() {
137
146
  console.log('Open Broker - Order History');
138
147
  console.log('==========================\n');
139
148
 
149
+ if (targetAddress) {
150
+ console.log(`Lookup: ${lookupAddress}\n`);
151
+ }
152
+
140
153
  if (orders.length === 0) {
141
154
  console.log('No orders found');
142
155
  return;
@@ -8,17 +8,21 @@ async function main() {
8
8
  const args = parseArgs(process.argv.slice(2));
9
9
  const filterCoin = args.coin as string | undefined;
10
10
  const jsonOutput = args.json as boolean;
11
+ const targetAddress = args.address as string | undefined;
11
12
  const client = getClient();
12
13
 
13
14
  if (args.verbose) {
14
15
  client.verbose = true;
15
16
  }
16
17
 
18
+ const lookupAddress = targetAddress?.toLowerCase();
19
+ const isOtherAccount = !!targetAddress;
20
+
17
21
  try {
18
22
  const [state, mids, fundingHistory] = await Promise.all([
19
- client.getUserStateAll(),
23
+ client.getUserStateAll(lookupAddress),
20
24
  client.getAllMids(),
21
- client.getUserFunding(),
25
+ client.getUserFunding(lookupAddress),
22
26
  ]);
23
27
 
24
28
  const positions = state.assetPositions.filter(ap => {
@@ -69,9 +73,13 @@ async function main() {
69
73
  console.log('Open Broker - Positions');
70
74
  console.log('=======================\n');
71
75
 
76
+ if (isOtherAccount) {
77
+ console.log(`Lookup: ${lookupAddress}\n`);
78
+ }
79
+
72
80
  if (positions.length === 0) {
73
81
  console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
74
- if (!filterCoin && !client.isApiWallet) {
82
+ if (!filterCoin && !isOtherAccount && !client.isApiWallet) {
75
83
  console.log('\n⚠️ If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
76
84
  console.log(' in ~/.openbroker/.env to the master account address.');
77
85
  }
@@ -8,6 +8,7 @@ interface Args {
8
8
  balances?: boolean;
9
9
  top?: number;
10
10
  verbose?: boolean;
11
+ address?: string;
11
12
  }
12
13
 
13
14
  function parseArgs(): Args {
@@ -21,6 +22,8 @@ function parseArgs(): Args {
21
22
  args.balances = true;
22
23
  } else if (arg === '--top' && process.argv[i + 1]) {
23
24
  args.top = parseInt(process.argv[++i], 10);
25
+ } else if (arg === '--address' && process.argv[i + 1]) {
26
+ args.address = process.argv[++i].toLowerCase();
24
27
  } else if (arg === '--verbose') {
25
28
  args.verbose = true;
26
29
  } else if (arg === '--help' || arg === '-h') {
@@ -30,11 +33,12 @@ Spot Markets - View Hyperliquid spot markets and balances
30
33
  Usage: npx tsx scripts/info/spot.ts [options]
31
34
 
32
35
  Options:
33
- --coin <symbol> Filter by coin symbol
34
- --balances Show your spot token balances
35
- --top <n> Show only top N markets by volume
36
- --verbose Show detailed output
37
- --help Show this help
36
+ --coin <symbol> Filter by coin symbol
37
+ --balances Show your spot token balances
38
+ --address <0x...> Look up another account's spot balances (with --balances)
39
+ --top <n> Show only top N markets by volume
40
+ --verbose Show detailed output
41
+ --help Show this help
38
42
 
39
43
  Examples:
40
44
  npx tsx scripts/info/spot.ts # Show all spot markets
@@ -80,9 +84,10 @@ async function main() {
80
84
 
81
85
  // Show balances
82
86
  if (args.balances) {
83
- console.log(`Fetching spot balances for ${client.address}...\n`);
87
+ const lookupAddress = args.address ?? client.address;
88
+ console.log(`Fetching spot balances for ${lookupAddress}...\n`);
84
89
 
85
- const balances = await client.getSpotBalances();
90
+ const balances = await client.getSpotBalances(args.address);
86
91
 
87
92
  if (!balances.balances || balances.balances.length === 0) {
88
93
  console.log('No spot token balances found.');