openbroker 1.0.82 → 1.0.85

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.82", "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.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)"}]}}
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
 
@@ -46,28 +46,52 @@ Or with the `ob_search` plugin tool: `{ "query": "gold" }` or `{ "query": "oil",
46
46
 
47
47
  **HIP-3 assets use `dex:COIN` format** — e.g., `xyz:CL` not just `CL`. If you get an error like "No market data found", search for the asset to find the correct prefixed ticker. Common HIP-3 dexes: `xyz`, `flx`, `km`, `hyna`, `vntl`, `cash`.
48
48
 
49
+ ### Asset IDs (disambiguation)
50
+
51
+ Every info JSON output includes an `assetId` field — the canonical Hyperliquid asset index. Prefer it over the coin name when persisting references, because the same ticker can exist on multiple providers (e.g. `HYPE` perp, `hyna:HYPE` HIP-3, and `HYPE/USDC` spot all coexist).
52
+
53
+ | Scope | Formula | Example |
54
+ |-------|---------|---------|
55
+ | Main perps | universe index | `HYPE` → `159` |
56
+ | HIP-3 perps | `100000 + dexIdx * 10000 + assetIdx` | `hyna:HYPE` → `140002` |
57
+ | Spot | `10000 + pair.index` | `HYPE/USDC` → `10107` |
58
+
59
+ ```bash
60
+ openbroker search HYPE --json | jq '.[] | {coin, assetId, type, provider}'
61
+ ```
62
+
63
+ Trading commands still take `--coin <name>` (including HIP-3 `dex:COIN`) — `assetId` is for queries, comparisons, and agent state, not order placement.
64
+
49
65
  ## Troubleshooting: CLI Fallback
50
66
 
51
67
  If an `ob_*` plugin tool returns unexpected errors, empty results, or crashes, **fall back to the equivalent CLI command** via Bash. The CLI and plugin tools share the same core code, but the CLI has more mature error handling and output.
52
68
 
69
+ **Every info command supports `--json`** for structured output. The table below covers the commands with dedicated plugin tools; any other info command (e.g. `spot`, `trades`, `fees`, `order-status`, `rate-limit`, `funding-history`, `all-markets`) can be run as `openbroker <command> --json` for the same effect.
70
+
53
71
  | Plugin Tool | CLI Equivalent |
54
72
  |-------------|---------------|
55
73
  | `ob_account` | `openbroker account --json` |
56
74
  | `ob_positions` | `openbroker positions --json` |
57
75
  | `ob_funding` | `openbroker funding --json --include-hip3` |
58
76
  | `ob_markets` | `openbroker markets --json --include-hip3` |
59
- | `ob_search` | `openbroker search --query <QUERY>` |
77
+ | `ob_search` | `openbroker search --query <QUERY> --json` |
78
+ | `ob_spot` | `openbroker spot --json` (or `--balances --json`) |
79
+ | `ob_fills` | `openbroker fills --json` |
80
+ | `ob_orders` | `openbroker orders --json` |
81
+ | `ob_order_status` | `openbroker order-status --oid <OID> --json` |
82
+ | `ob_fees` | `openbroker fees --json` |
83
+ | `ob_candles` | `openbroker candles --coin <COIN> --json` |
84
+ | `ob_funding_history` | `openbroker funding-history --coin <COIN> --json` |
85
+ | `ob_trades` | `openbroker trades --coin <COIN> --json` |
86
+ | `ob_rate_limit` | `openbroker rate-limit --json` |
87
+ | `ob_funding_scan` | `openbroker funding-scan --json` |
60
88
  | `ob_buy` | `openbroker buy --coin <COIN> --size <SIZE>` |
61
89
  | `ob_sell` | `openbroker sell --coin <COIN> --size <SIZE>` |
62
90
  | `ob_limit` | `openbroker limit --coin <COIN> --side <SIDE> --size <SIZE> --price <PRICE>` |
63
91
  | `ob_tpsl` | `openbroker tpsl --coin <COIN> --tp <PRICE> --sl <PRICE>` |
64
92
  | `ob_cancel` | `openbroker cancel --all` or `--coin <COIN>` |
65
- | `ob_fills` | `openbroker fills --json` |
66
- | `ob_orders` | `openbroker orders --json` |
67
- | `ob_funding_scan` | `openbroker funding-scan --json` |
68
- | `ob_candles` | `openbroker candles --coin <COIN> --json` |
69
93
  | `ob_auto_run` | `openbroker auto run <script> [--dry]` |
70
- | `ob_auto_stop` | (stop via SIGINT when using CLI) |
94
+ | `ob_auto_stop` | `openbroker auto stop <id>` (or SIGINT if run in foreground) |
71
95
  | `ob_auto_list` | `openbroker auto list` |
72
96
 
73
97
  **When to use CLI fallback:**
@@ -137,12 +161,14 @@ openbroker positions --address 0xabc... # Another account's positions
137
161
  ```bash
138
162
  openbroker funding --top 20 # Top 20 by funding rate
139
163
  openbroker funding --coin ETH # Specific coin
164
+ openbroker funding --top 20 --json # JSON (includes assetId)
140
165
  ```
141
166
 
142
167
  ### Markets
143
168
  ```bash
144
169
  openbroker markets --top 30 # Top 30 main perps
145
170
  openbroker markets --coin BTC # Specific coin
171
+ openbroker markets --coin BTC --json # JSON (includes assetId)
146
172
  ```
147
173
 
148
174
  ### All Markets (Perps + Spot + HIP-3)
@@ -152,6 +178,7 @@ openbroker all-markets --type perp # Main perps only
152
178
  openbroker all-markets --type hip3 # HIP-3 perps only
153
179
  openbroker all-markets --type spot # Spot markets only
154
180
  openbroker all-markets --top 20 # Top 20 by volume
181
+ openbroker all-markets --json # JSON (includes assetId)
155
182
  ```
156
183
 
157
184
  ### Search Markets (Find assets across providers)
@@ -159,6 +186,7 @@ openbroker all-markets --top 20 # Top 20 by volume
159
186
  openbroker search --query GOLD # Find all GOLD markets
160
187
  openbroker search --query BTC # Find BTC across all providers
161
188
  openbroker search --query ETH --type perp # ETH perps only
189
+ openbroker search HYPE --json # JSON with assetId per result
162
190
  ```
163
191
 
164
192
  ### Spot Markets
@@ -168,6 +196,7 @@ openbroker spot --coin PURR # Show PURR market info
168
196
  openbroker spot --balances # Show your spot balances
169
197
  openbroker spot --balances --address 0xabc... # Another account's spot balances
170
198
  openbroker spot --top 20 # Top 20 by volume
199
+ openbroker spot --json # JSON (includes assetId, base, quote)
171
200
  ```
172
201
 
173
202
  ### Trade Fills
@@ -193,12 +222,14 @@ openbroker orders --address 0xabc... --open # Another account's open orders
193
222
  openbroker order-status --oid 123456789 # Check specific order
194
223
  openbroker order-status --oid 0x1234... # By client order ID
195
224
  openbroker order-status --oid 123456789 --address 0xabc... # On another account
225
+ openbroker order-status --oid 123456789 --json
196
226
  ```
197
227
 
198
228
  ### Fee Schedule
199
229
  ```bash
200
230
  openbroker fees # Fee tier, rates, and volume
201
231
  openbroker fees --address 0xabc... # Another account's fees
232
+ openbroker fees --json
202
233
  ```
203
234
 
204
235
  ### Candle Data (OHLCV)
@@ -206,23 +237,27 @@ openbroker fees --address 0xabc... # Another account's fees
206
237
  openbroker candles --coin ETH # 24 hourly candles
207
238
  openbroker candles --coin BTC --interval 4h --bars 48 # 48 four-hour bars
208
239
  openbroker candles --coin SOL --interval 1d --bars 30 # 30 daily bars
240
+ openbroker candles --coin ETH --json # JSON (coin, assetId, interval, candles)
209
241
  ```
210
242
 
211
243
  ### Funding History
212
244
  ```bash
213
245
  openbroker funding-history --coin ETH # Last 24h
214
246
  openbroker funding-history --coin BTC --hours 168 # Last 7 days
247
+ openbroker funding-history --coin ETH --json # JSON (coin, assetId, history)
215
248
  ```
216
249
 
217
250
  ### Recent Trades (Tape)
218
251
  ```bash
219
252
  openbroker trades --coin ETH # Last 30 trades
220
253
  openbroker trades --coin BTC --top 50 # Last 50 trades
254
+ openbroker trades --coin ETH --json # JSON (coin, assetId, trades)
221
255
  ```
222
256
 
223
257
  ### Rate Limit
224
258
  ```bash
225
259
  openbroker rate-limit # API usage and capacity
260
+ openbroker rate-limit --json
226
261
  ```
227
262
 
228
263
  ### Funding Rate Scanner (Cross-Dex)
@@ -231,6 +266,7 @@ openbroker funding-scan # Scan all dexes, >25% threshol
231
266
  openbroker funding-scan --threshold 50 --pairs # Show opposing funding pairs
232
267
  openbroker funding-scan --hip3-only --top 20 # HIP-3 only
233
268
  openbroker funding-scan --watch --interval 120 # Re-scan every 2 minutes
269
+ openbroker funding-scan --json # JSON (includes assetId per result)
234
270
  ```
235
271
 
236
272
  ## Trading Commands
@@ -293,6 +329,27 @@ openbroker cancel --coin ETH # Cancel ETH orders only
293
329
  openbroker cancel --oid 123456 # Cancel specific order
294
330
  ```
295
331
 
332
+ ### Spot Orders
333
+ Spot trading uses a separate order path with its own asset indices (see Asset IDs section). Pass the base token symbol as `--coin` — quote is always USDC.
334
+
335
+ ```bash
336
+ # Market orders (shortcuts)
337
+ openbroker spot-buy --coin PURR --size 1000
338
+ openbroker spot-sell --coin HYPE --size 5
339
+
340
+ # Full form (specify --side)
341
+ openbroker spot-order --coin PURR --side buy --size 1000
342
+
343
+ # Limit orders (add --price)
344
+ openbroker spot-order --coin HYPE --side sell --size 50 --price 25.50
345
+ openbroker spot-order --coin PURR --side buy --size 500 --price 0.20 --tif Alo
346
+
347
+ # Preview without executing
348
+ openbroker spot-buy --coin PURR --size 500 --dry
349
+ ```
350
+
351
+ **Spot flags:** `--coin`, `--side`, `--size`, `--price` (omit → market order), `--tif` (`Gtc`/`Ioc`/`Alo`, default `Gtc`), `--slippage` (bps, market orders only), `--dry`, `--verbose`.
352
+
296
353
  ## Advanced Execution
297
354
 
298
355
  ### TWAP (Native Exchange Order)
@@ -303,6 +360,9 @@ openbroker twap --coin ETH --side buy --size 1 --duration 30
303
360
  # Sell 0.5 BTC over 2 hours without randomized timing
304
361
  openbroker twap --coin BTC --side sell --size 0.5 --duration 120 --randomize false
305
362
 
363
+ # Reduce-only (close position with TWAP). Note: TWAP uses `--reduce-only`, not `--reduce`
364
+ openbroker twap --coin ETH --side sell --size 1 --duration 30 --reduce-only
365
+
306
366
  # Cancel a running TWAP
307
367
  openbroker twap-cancel --coin ETH --twap-id 77738308
308
368
 
@@ -407,6 +467,7 @@ Run `openbroker setup` to create the global config interactively.
407
467
  | `HYPERLIQUID_PRIVATE_KEY` | Yes | Wallet private key (0x...) |
408
468
  | `HYPERLIQUID_NETWORK` | No | `mainnet` (default) or `testnet` |
409
469
  | `HYPERLIQUID_ACCOUNT_ADDRESS` | No | Master account address (required for API wallets) |
470
+ | `OB_DASHBOARD_URL` | No | Dashboard API URL for forwarding audit notes, metrics, and agent actions (e.g. `http://localhost:3001`) |
410
471
 
411
472
  The builder fee (1 bps / 0.01%) is hardcoded and not configurable.
412
473
 
@@ -591,6 +652,8 @@ export default function(api) {
591
652
 
592
653
  Automations now write a local audit trail automatically to `~/.openbroker/automation-audit.sqlite`. The runtime records run config, logs, state changes, write actions, order updates, fills, user events, and per-poll account snapshots so you can generate performance reports later.
593
654
 
655
+ **Dashboard Forwarding:** When `OB_DASHBOARD_URL` is set (e.g. `http://localhost:3001`), audit notes, metrics, and trade actions are automatically forwarded to the OpenBroker Vaults dashboard API in real time. The vault address is read from `HYPERSTABLE_VAULT_ADDRESS` or `VAULT`. Forwarding is fire-and-forget — it never blocks the automation or causes errors if the dashboard is unreachable.
656
+
594
657
  ### Events
595
658
 
596
659
  | Event | Payload | When |
@@ -1053,10 +1116,14 @@ export default function(api) {
1053
1116
  openbroker auto run my-strategy --dry # Test without trading
1054
1117
  openbroker auto run ./funding-scalp.ts # Run from path
1055
1118
  openbroker auto run my-strategy --poll 5000 # Poll every 5s
1119
+ openbroker auto run my-strategy --no-ws # Disable WebSocket, pure REST polling
1056
1120
  openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry # Run bundled example
1057
1121
  openbroker auto examples # List bundled examples with config
1058
1122
  openbroker auto list # Show available scripts
1059
1123
  openbroker auto status # Show running automations
1124
+ openbroker auto stop <id> # Unregister an automation (won't auto-restart)
1125
+ openbroker auto report <id> # Read the local audit report (logs, trades, metrics) for an automation
1126
+ openbroker auto clean # Remove stale entries from the registry
1060
1127
  ```
1061
1128
 
1062
1129
  **Plugin tools (for OpenClaw agents):**
@@ -1072,9 +1139,14 @@ openbroker auto status # Show running automations
1072
1139
  | `--verbose` | Show debug output | false |
1073
1140
  | `--id <name>` | Custom automation ID | filename |
1074
1141
  | `--poll <ms>` | Poll interval in milliseconds | 10000 |
1142
+ | `--no-ws` | Disable WebSocket; fall back to REST-only polling | WebSocket on |
1075
1143
  | `--example <name>` | Run a bundled example automation | - |
1076
1144
  | `--set key=value` | Set config values (repeatable) | - |
1077
1145
 
1146
+ **Inspecting automations after they run:**
1147
+ - `openbroker auto report <id>` — reads the local SQLite audit trail at `~/.openbroker/automation-audit.sqlite` and prints a summary of logs, write actions, fills, PnL, and custom metrics recorded via `api.audit.record()` / `api.audit.metric()`. Use this to review what a strategy actually did.
1148
+ - `openbroker auto clean` — prunes registry entries for automations that are no longer running or whose script file is gone. Safe to run anytime.
1149
+
1078
1150
  **Guidelines for agents writing automations:**
1079
1151
 
1080
1152
  **Risk & Safety (mandatory):**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.82",
4
+ "version": "1.0.85",
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.82",
3
+ "version": "1.0.85",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@ Options (for run):
32
32
  --verbose Show debug output
33
33
  --id <name> Custom automation ID (default: filename)
34
34
  --poll <ms> Poll interval in milliseconds (default: 10000)
35
+ --no-ws Disable WebSocket; fall back to REST-only polling
35
36
 
36
37
  Scripts are loaded from:
37
38
  1. Absolute or relative path
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Dashboard audit forwarder.
3
+ *
4
+ * When OB_DASHBOARD_URL is set, wraps the AutomationAudit API to also POST
5
+ * audit notes, metrics, and agent action logs to the ob-app backend.
6
+ *
7
+ * Fires HTTP requests in the background — never blocks the automation loop.
8
+ */
9
+
10
+ import type { AutomationAudit } from './types.js';
11
+
12
+ const DASHBOARD_URL = process.env.OB_DASHBOARD_URL; // e.g. "http://localhost:3001"
13
+ const DASHBOARD_API_KEY = process.env.OB_DASHBOARD_API_KEY || '';
14
+ const VAULT_ADDRESS = process.env.HYPERSTABLE_VAULT_ADDRESS || process.env.VAULT || '';
15
+
16
+ function postJSON(path: string, body: unknown): void {
17
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return;
18
+
19
+ const url = `${DASHBOARD_URL}/api/vaults/${VAULT_ADDRESS.toLowerCase()}${path}`;
20
+
21
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
22
+ if (DASHBOARD_API_KEY) {
23
+ headers['Authorization'] = `Bearer ${DASHBOARD_API_KEY}`;
24
+ }
25
+
26
+ fetch(url, {
27
+ method: 'POST',
28
+ headers,
29
+ body: JSON.stringify(body),
30
+ signal: AbortSignal.timeout(5_000),
31
+ }).catch(() => {
32
+ // Silently ignore — dashboard may be down, automation must not be affected.
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Wrap an existing AutomationAudit to forward calls to the dashboard API.
38
+ * If OB_DASHBOARD_URL is not set, returns the original audit object unchanged.
39
+ */
40
+ export function withDashboardForwarder(audit: AutomationAudit): AutomationAudit {
41
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return audit;
42
+
43
+ return {
44
+ record(kind: string, payload?: unknown): void {
45
+ audit.record(kind, payload);
46
+ postJSON('/audit/notes', {
47
+ category: kind,
48
+ label: typeof payload === 'object' && payload !== null && 'reason' in payload
49
+ ? String((payload as Record<string, unknown>).reason)
50
+ : kind,
51
+ data: payload ?? {},
52
+ });
53
+ },
54
+
55
+ metric(name: string, value: number, tags?: Record<string, unknown>): void {
56
+ audit.metric(name, value, tags);
57
+ postJSON('/audit/metrics', {
58
+ name,
59
+ value,
60
+ tags: tags ?? {},
61
+ });
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Forward an agent action log to the dashboard.
68
+ * Call this from audited client wrappers or directly from automation code.
69
+ */
70
+ export function forwardAgentAction(
71
+ action: string,
72
+ status: 'success' | 'error' | 'pending',
73
+ details: Record<string, unknown>,
74
+ txHash?: string,
75
+ ): void {
76
+ postJSON('/agent/logs', { action, status, details, txHash });
77
+ }
@@ -22,8 +22,10 @@ export function resolveScriptPath(nameOrPath: string): string {
22
22
  return nameOrPath;
23
23
  }
24
24
 
25
- // Relative to cwd
26
- const cwdPath = path.resolve(process.cwd(), nameOrPath);
25
+ // Relative to the user's original cwd (not the openbroker package root that
26
+ // `bin/openbroker.js` chdirs to before spawning tsx).
27
+ const userCwd = process.env.OPENBROKER_CWD || process.cwd();
28
+ const cwdPath = path.resolve(userCwd, nameOrPath);
27
29
  if (existsSync(cwdPath)) return cwdPath;
28
30
 
29
31
  // Relative to ~/.openbroker/automations/
@@ -15,6 +15,7 @@ import { AutomationEventBus } from './events.js';
15
15
  import { loadAutomation } from './loader.js';
16
16
  import { registerAutomation, unregisterAutomation, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
17
17
  import { createAutomationAudit, toSerializable, type AutomationAuditSink } from './audit.js';
18
+ import { withDashboardForwarder, forwardAgentAction } from './dashboard-forwarder.js';
18
19
  import type {
19
20
  AutomationAPI,
20
21
  AutomationEventPayloads,
@@ -178,6 +179,11 @@ function createAuditedClient(
178
179
  result,
179
180
  dryRun,
180
181
  });
182
+ forwardAgentAction(
183
+ prop,
184
+ 'success',
185
+ { args: toSerializable(args), result: toSerializable(result), dryRun },
186
+ );
181
187
  return result;
182
188
  } catch (error) {
183
189
  audit.recordAction({
@@ -187,6 +193,11 @@ function createAuditedClient(
187
193
  error,
188
194
  dryRun,
189
195
  });
196
+ forwardAgentAction(
197
+ prop,
198
+ 'error',
199
+ { args: toSerializable(args), error: String(error), dryRun },
200
+ );
190
201
  throw error;
191
202
  }
192
203
  };
@@ -424,10 +435,10 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
424
435
 
425
436
  // Build the API object
426
437
  const publish = createAuditedPublish(createPublish(id, log, gatewayPort, hooksToken), audit);
427
- const auditApi: AutomationAudit = {
438
+ const auditApi: AutomationAudit = withDashboardForwarder({
428
439
  record: (kind: string, payload?: unknown) => audit.recordNote(kind, payload),
429
440
  metric: (name: string, value: number, tags?: Record<string, unknown>) => audit.recordMetric(name, value, tags),
430
- };
441
+ });
431
442
  const api: AutomationAPI = {
432
443
  client,
433
444
  utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
@@ -112,7 +112,7 @@ export function loadConfig(): OpenBrokerConfig {
112
112
  // Show warning once
113
113
  if (!readOnlyWarningShown) {
114
114
  readOnlyWarningShown = true;
115
- console.log('\x1b[33m⚠️ Not configured for trading. Run "openbroker setup" to enable trades.\x1b[0m\n');
115
+ console.error('\x1b[33m⚠️ Not configured for trading. Run "openbroker setup" to enable trades.\x1b[0m\n');
116
116
  }
117
117
  }
118
118
 
@@ -7,6 +7,7 @@ interface Args {
7
7
  type?: 'perp' | 'spot' | 'hip3' | 'all';
8
8
  top?: number;
9
9
  verbose?: boolean;
10
+ json?: boolean;
10
11
  }
11
12
 
12
13
  function parseArgs(): Args {
@@ -23,6 +24,8 @@ function parseArgs(): Args {
23
24
  args.top = parseInt(process.argv[++i], 10);
24
25
  } else if (arg === '--verbose') {
25
26
  args.verbose = true;
27
+ } else if (arg === '--json') {
28
+ args.json = true;
26
29
  } else if (arg === '--help' || arg === '-h') {
27
30
  console.log(`
28
31
  All Markets - View all available markets on Hyperliquid
@@ -32,6 +35,7 @@ Usage: npx tsx scripts/info/all-markets.ts [options]
32
35
  Options:
33
36
  --type <type> Market type: perp, spot, hip3, or all (default: all)
34
37
  --top <n> Show only top N markets by volume
38
+ --json Output as JSON (machine-readable)
35
39
  --verbose Show detailed output
36
40
  --help Show this help
37
41
 
@@ -41,6 +45,7 @@ Examples:
41
45
  npx tsx scripts/info/all-markets.ts --type hip3 # Show only HIP-3 perps
42
46
  npx tsx scripts/info/all-markets.ts --type spot # Show only spot markets
43
47
  npx tsx scripts/info/all-markets.ts --top 20 # Show top 20 by volume
48
+ npx tsx scripts/info/all-markets.ts --json # JSON output
44
49
  `);
45
50
  process.exit(0);
46
51
  }
@@ -71,22 +76,27 @@ function formatFunding(rate: string): string {
71
76
  return `${sign}${annualized.toFixed(2)}%`;
72
77
  }
73
78
 
79
+ interface MarketRow {
80
+ type: 'perp' | 'spot' | 'hip3';
81
+ provider: string;
82
+ coin: string;
83
+ assetId: number;
84
+ price: string;
85
+ volume24h: number;
86
+ funding?: string;
87
+ maxLeverage?: number;
88
+ }
89
+
74
90
  async function main() {
75
91
  const args = parseArgs();
76
92
  const client = getClient();
77
93
  client.verbose = args.verbose ?? false;
78
94
 
79
- console.log('Fetching market data...\n');
95
+ if (!args.json) {
96
+ console.log('Fetching market data...\n');
97
+ }
80
98
 
81
- const allMarkets: Array<{
82
- type: 'perp' | 'spot' | 'hip3';
83
- provider: string;
84
- coin: string;
85
- price: string;
86
- volume24h: number;
87
- funding?: string;
88
- maxLeverage?: number;
89
- }> = [];
99
+ const allMarkets: MarketRow[] = [];
90
100
 
91
101
  // Fetch main perps
92
102
  if (args.type === 'all' || args.type === 'perp') {
@@ -98,6 +108,7 @@ async function main() {
98
108
  type: 'perp',
99
109
  provider: 'Hyperliquid',
100
110
  coin: asset.name,
111
+ assetId: i,
101
112
  price: ctx.markPx,
102
113
  volume24h: parseFloat(ctx.dayNtlVlm),
103
114
  funding: ctx.funding,
@@ -108,7 +119,7 @@ async function main() {
108
119
 
109
120
  // Fetch HIP-3 perps
110
121
  if (args.type === 'all' || args.type === 'hip3') {
111
- if (client.isTestnet) {
122
+ if (client.isTestnet && !args.json) {
112
123
  console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Use "dexName:COIN" syntax to load a specific dex.\n');
113
124
  }
114
125
  try {
@@ -123,10 +134,13 @@ async function main() {
123
134
  const ctx = dexData.assetCtxs[i];
124
135
  if (!asset || !ctx) continue;
125
136
 
137
+ let assetId = -1;
138
+ try { assetId = client.getAssetIndex(asset.name); } catch { /* not registered */ }
126
139
  allMarkets.push({
127
140
  type: 'hip3',
128
141
  provider: dexData.dexName || `HIP-3 DEX ${dexIdx}`,
129
142
  coin: asset.name,
143
+ assetId,
130
144
  price: ctx.markPx,
131
145
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
132
146
  funding: ctx.funding,
@@ -143,15 +157,23 @@ async function main() {
143
157
  if (args.type === 'all' || args.type === 'spot') {
144
158
  try {
145
159
  const spotData = await client.getSpotMetaAndAssetCtxs();
146
- for (let i = 0; i < spotData.meta.universe.length; i++) {
147
- const pair = spotData.meta.universe[i];
148
- const ctx = spotData.assetCtxs[i];
149
- if (!pair || !ctx) continue;
160
+ // contexts carry coin field that matches pair.name; not necessarily aligned by index
161
+ const ctxMap = new Map<string, (typeof spotData.assetCtxs)[number]>();
162
+ for (const ctx of spotData.assetCtxs) {
163
+ if ((ctx as Record<string, unknown>).coin) {
164
+ ctxMap.set((ctx as Record<string, unknown>).coin as string, ctx);
165
+ }
166
+ }
167
+ for (const pair of spotData.meta.universe) {
168
+ if (!pair) continue;
169
+ const ctx = ctxMap.get(pair.name);
170
+ if (!ctx) continue;
150
171
 
151
172
  allMarkets.push({
152
173
  type: 'spot',
153
174
  provider: 'Spot',
154
175
  coin: pair.name,
176
+ assetId: 10000 + pair.index,
155
177
  price: ctx.markPx,
156
178
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
157
179
  });
@@ -167,6 +189,11 @@ async function main() {
167
189
  // Apply top filter
168
190
  const markets = args.top ? allMarkets.slice(0, args.top) : allMarkets;
169
191
 
192
+ if (args.json) {
193
+ console.log(JSON.stringify(markets, null, 2));
194
+ return;
195
+ }
196
+
170
197
  // Group by type for display
171
198
  const perps = markets.filter((m) => m.type === 'perp');
172
199
  const hip3 = markets.filter((m) => m.type === 'hip3');
@@ -183,11 +210,11 @@ async function main() {
183
210
  // Print perps
184
211
  if (perps.length > 0) {
185
212
  console.log('=== Main Perpetuals ===\n');
186
- console.log('Coin Price 24h Volume Funding (Ann.) Leverage');
187
- console.log('-'.repeat(75));
213
+ console.log('Coin AssetID Price 24h Volume Funding (Ann.) Leverage');
214
+ console.log('-'.repeat(87));
188
215
  for (const m of perps) {
189
216
  console.log(
190
- `${m.coin.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${(m.maxLeverage ? `${m.maxLeverage}x` : '-').padStart(9)}`
217
+ `${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${(m.maxLeverage ? `${m.maxLeverage}x` : '-').padStart(9)}`
191
218
  );
192
219
  }
193
220
  console.log();
@@ -196,11 +223,11 @@ async function main() {
196
223
  // Print HIP-3 markets
197
224
  if (hip3.length > 0) {
198
225
  console.log('=== HIP-3 Perpetuals ===\n');
199
- console.log('Coin Provider Price 24h Volume Funding (Ann.)');
200
- console.log('-'.repeat(80));
226
+ console.log('Coin Provider AssetID Price 24h Volume Funding (Ann.)');
227
+ console.log('-'.repeat(92));
201
228
  for (const m of hip3) {
202
229
  console.log(
203
- `${m.coin.padEnd(14)} ${m.provider.padEnd(16)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)}`
230
+ `${m.coin.padEnd(14)} ${m.provider.padEnd(16)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)}`
204
231
  );
205
232
  }
206
233
  console.log();
@@ -209,11 +236,11 @@ async function main() {
209
236
  // Print spot markets
210
237
  if (spots.length > 0) {
211
238
  console.log('=== Spot Markets ===\n');
212
- console.log('Pair Price 24h Volume');
213
- console.log('-'.repeat(50));
239
+ console.log('Pair AssetID Price 24h Volume');
240
+ console.log('-'.repeat(62));
214
241
  for (const m of spots) {
215
242
  console.log(
216
- `${m.coin.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)}`
243
+ `${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)}`
217
244
  );
218
245
  }
219
246
  console.log();
@@ -21,6 +21,7 @@ Options:
21
21
  --interval <interval> Candle interval (default: 1h)
22
22
  Valid: ${VALID_INTERVALS.join(', ')}
23
23
  --bars <n> Number of bars to fetch (default: 24)
24
+ --json Output as JSON (machine-readable)
24
25
  --help, -h Show this help
25
26
 
26
27
  Examples:
@@ -59,10 +60,13 @@ async function main() {
59
60
  }
60
61
 
61
62
  const bars = parseInt(args.bars as string) || 24;
63
+ const jsonOutput = args.json as boolean;
62
64
  const client = getClient();
63
65
 
64
- console.log(`Open Broker - ${normalizeCoin(coin)} Candles (${interval})`);
65
- console.log('='.repeat(40) + '\n');
66
+ if (!jsonOutput) {
67
+ console.log(`Open Broker - ${normalizeCoin(coin)} Candles (${interval})`);
68
+ console.log('='.repeat(40) + '\n');
69
+ }
66
70
 
67
71
  try {
68
72
  // Load metadata (needed for HIP-3 coin resolution)
@@ -75,6 +79,13 @@ async function main() {
75
79
  const startTime = now - (bars * (INTERVAL_MS[interval] || 3_600_000));
76
80
  const candles = await client.getCandleSnapshot(normalizeCoin(coin), interval, startTime);
77
81
 
82
+ if (jsonOutput) {
83
+ let assetId = -1;
84
+ try { assetId = client.getAssetIndex(normalizeCoin(coin)); } catch { /* noop */ }
85
+ console.log(JSON.stringify({ coin: normalizeCoin(coin), assetId, interval, candles }, null, 2));
86
+ return;
87
+ }
88
+
78
89
  if (candles.length === 0) {
79
90
  console.log('No candle data found');
80
91
  return;
@@ -9,10 +9,13 @@ function printUsage() {
9
9
  Usage: openbroker fees [options]
10
10
 
11
11
  Options:
12
- --help, -h Show this help
12
+ --address <0x...> Look up another account's fees
13
+ --json Output as JSON (machine-readable)
14
+ --help, -h Show this help
13
15
 
14
16
  Examples:
15
17
  openbroker fees
18
+ openbroker fees --json
16
19
  `);
17
20
  }
18
21
 
@@ -25,19 +28,27 @@ async function main() {
25
28
  }
26
29
 
27
30
  const targetAddress = args.address as string | undefined;
31
+ const jsonOutput = args.json as boolean;
28
32
  const client = getClient();
29
33
  const lookupAddress = targetAddress?.toLowerCase();
30
34
 
31
- console.log('Open Broker - Fee Schedule');
32
- console.log('=========================\n');
35
+ if (!jsonOutput) {
36
+ console.log('Open Broker - Fee Schedule');
37
+ console.log('=========================\n');
33
38
 
34
- if (targetAddress) {
35
- console.log(`Lookup: ${lookupAddress}\n`);
39
+ if (targetAddress) {
40
+ console.log(`Lookup: ${lookupAddress}\n`);
41
+ }
36
42
  }
37
43
 
38
44
  try {
39
45
  const fees = await client.getUserFees(lookupAddress);
40
46
 
47
+ if (jsonOutput) {
48
+ console.log(JSON.stringify(fees, null, 2));
49
+ return;
50
+ }
51
+
41
52
  // Fee rates
42
53
  console.log('Fee Rates');
43
54
  console.log('─'.repeat(40));
@@ -11,6 +11,7 @@ Usage: openbroker funding-history --coin <symbol> [options]
11
11
  Options:
12
12
  --coin <symbol> Asset symbol (required, e.g. ETH, BTC)
13
13
  --hours <n> Hours of history to fetch (default: 24)
14
+ --json Output as JSON (machine-readable)
14
15
  --help, -h Show this help
15
16
 
16
17
  Examples:
@@ -35,10 +36,13 @@ async function main() {
35
36
  }
36
37
 
37
38
  const hours = parseInt(args.hours as string) || 24;
39
+ const jsonOutput = args.json as boolean;
38
40
  const client = getClient();
39
41
 
40
- console.log(`Open Broker - ${normalizeCoin(coin)} Funding History (${hours}h)`);
41
- console.log('='.repeat(40) + '\n');
42
+ if (!jsonOutput) {
43
+ console.log(`Open Broker - ${normalizeCoin(coin)} Funding History (${hours}h)`);
44
+ console.log('='.repeat(40) + '\n');
45
+ }
42
46
 
43
47
  try {
44
48
  // Load metadata (needed for HIP-3 coin resolution)
@@ -51,6 +55,13 @@ async function main() {
51
55
  const startTime = now - (hours * 3_600_000);
52
56
  const history = await client.getFundingHistory(normalizeCoin(coin), startTime);
53
57
 
58
+ if (jsonOutput) {
59
+ let assetId = -1;
60
+ try { assetId = client.getAssetIndex(normalizeCoin(coin)); } catch { /* noop */ }
61
+ console.log(JSON.stringify({ coin: normalizeCoin(coin), assetId, history }, null, 2));
62
+ return;
63
+ }
64
+
54
65
  if (history.length === 0) {
55
66
  console.log('No funding history found');
56
67
  return;
@@ -32,6 +32,7 @@ Examples:
32
32
 
33
33
  interface FundingScanResult {
34
34
  coin: string;
35
+ assetId: number;
35
36
  dex: string;
36
37
  hourlyRate: number;
37
38
  annualizedPct: number;
@@ -69,9 +70,14 @@ async function scanFunding(client: ReturnType<typeof getClient>, options: {
69
70
 
70
71
  // API returns HIP-3 names already prefixed (e.g., "xyz:CL")
71
72
  const coin = asset.name;
73
+ let assetId = isMain ? i : -1;
74
+ if (!isMain) {
75
+ try { assetId = client.getAssetIndex(coin); } catch { /* not registered */ }
76
+ }
72
77
 
73
78
  results.push({
74
79
  coin,
80
+ assetId,
75
81
  dex: dexData.dexName ?? 'main',
76
82
  hourlyRate,
77
83
  annualizedPct,
@@ -6,6 +6,7 @@ import { formatPercent, annualizeFundingRate, parseArgs } from '../core/utils.js
6
6
 
7
7
  interface FundingDisplay {
8
8
  coin: string;
9
+ assetId: number;
9
10
  hourlyRate: number;
10
11
  annualizedRate: number;
11
12
  premium: number;
@@ -47,6 +48,7 @@ async function main() {
47
48
 
48
49
  fundingData.push({
49
50
  coin: asset.name,
51
+ assetId: i,
50
52
  hourlyRate,
51
53
  annualizedRate,
52
54
  premium,
@@ -84,8 +86,12 @@ async function main() {
84
86
 
85
87
  if (!showAll && openInterest < 1000) continue;
86
88
 
89
+ let assetId = -1;
90
+ try { assetId = client.getAssetIndex(coinName); } catch { /* not registered */ }
91
+
87
92
  fundingData.push({
88
93
  coin: coinName,
94
+ assetId,
89
95
  hourlyRate,
90
96
  annualizedRate,
91
97
  premium: 0,
@@ -6,6 +6,7 @@ import { formatUsd, parseArgs } from '../core/utils.js';
6
6
 
7
7
  interface MarketDisplay {
8
8
  coin: string;
9
+ assetId: number;
9
10
  markPx: number;
10
11
  oraclePx: number;
11
12
  prevDayPx: number;
@@ -47,6 +48,7 @@ async function main() {
47
48
 
48
49
  markets.push({
49
50
  coin: asset.name,
51
+ assetId: i,
50
52
  markPx,
51
53
  oraclePx,
52
54
  prevDayPx,
@@ -86,9 +88,12 @@ async function main() {
86
88
  const volume24h = parseFloat(ctx.dayNtlVlm);
87
89
  const openInterest = parseFloat(ctx.openInterest);
88
90
  const change24h = prevDayPx > 0 ? (markPx - prevDayPx) / prevDayPx : 0;
91
+ let assetId = -1;
92
+ try { assetId = client.getAssetIndex(coinName); } catch { /* not registered */ }
89
93
 
90
94
  markets.push({
91
95
  coin: coinName,
96
+ assetId,
92
97
  markPx,
93
98
  oraclePx,
94
99
  prevDayPx,
@@ -11,6 +11,7 @@ Usage: openbroker order-status --oid <order-id>
11
11
  Options:
12
12
  --oid <id> Order ID (number) or client order ID (hex string) — required
13
13
  --address <0x...> Look up order on another account
14
+ --json Output as JSON (machine-readable)
14
15
  --help, -h Show this help
15
16
 
16
17
  Examples:
@@ -37,15 +38,23 @@ async function main() {
37
38
 
38
39
  const oid = oidArg.startsWith('0x') ? oidArg : parseInt(oidArg);
39
40
  const targetAddress = args.address as string | undefined;
41
+ const jsonOutput = args.json as boolean;
40
42
  const client = getClient();
41
43
  const lookupAddress = targetAddress?.toLowerCase();
42
44
 
43
- console.log('Open Broker - Order Status');
44
- console.log('=========================\n');
45
+ if (!jsonOutput) {
46
+ console.log('Open Broker - Order Status');
47
+ console.log('=========================\n');
48
+ }
45
49
 
46
50
  try {
47
51
  const result = await client.getOrderStatus(oid, lookupAddress);
48
52
 
53
+ if (jsonOutput) {
54
+ console.log(JSON.stringify(result, null, 2));
55
+ return;
56
+ }
57
+
49
58
  if (result.status === 'unknownOid') {
50
59
  console.log(`Order ${oidArg} not found`);
51
60
  return;
@@ -9,6 +9,7 @@ function printUsage() {
9
9
  Usage: openbroker rate-limit [options]
10
10
 
11
11
  Options:
12
+ --json Output as JSON (machine-readable)
12
13
  --help, -h Show this help
13
14
 
14
15
  Examples:
@@ -24,14 +25,22 @@ async function main() {
24
25
  process.exit(0);
25
26
  }
26
27
 
28
+ const jsonOutput = args.json as boolean;
27
29
  const client = getClient();
28
30
 
29
- console.log('Open Broker - API Rate Limit');
30
- console.log('===========================\n');
31
+ if (!jsonOutput) {
32
+ console.log('Open Broker - API Rate Limit');
33
+ console.log('===========================\n');
34
+ }
31
35
 
32
36
  try {
33
37
  const rl = await client.getUserRateLimit();
34
38
 
39
+ if (jsonOutput) {
40
+ console.log(JSON.stringify(rl, null, 2));
41
+ return;
42
+ }
43
+
35
44
  const used = rl.nRequestsUsed;
36
45
  const cap = rl.nRequestsCap;
37
46
  const surplus = rl.nRequestsSurplus;
@@ -7,6 +7,7 @@ interface Args {
7
7
  query: string;
8
8
  type?: 'perp' | 'spot' | 'hip3' | 'all';
9
9
  verbose?: boolean;
10
+ json?: boolean;
10
11
  }
11
12
 
12
13
  function parseArgs(): Args {
@@ -23,6 +24,8 @@ function parseArgs(): Args {
23
24
  }
24
25
  } else if (arg === '--verbose') {
25
26
  args.verbose = true;
27
+ } else if (arg === '--json') {
28
+ args.json = true;
26
29
  } else if (arg === '--help' || arg === '-h') {
27
30
  console.log(`
28
31
  Search Markets - Find assets across all Hyperliquid markets
@@ -32,6 +35,7 @@ Usage: npx tsx scripts/info/search-markets.ts --query <search> [options]
32
35
  Options:
33
36
  --query <search> Search term (required) - matches coin name
34
37
  --type <type> Filter by market type: perp, spot, hip3, or all (default: all)
38
+ --json Output as JSON (machine-readable)
35
39
  --verbose Show detailed output
36
40
  --help Show this help
37
41
 
@@ -40,6 +44,7 @@ Examples:
40
44
  npx tsx scripts/info/search-markets.ts --query BTC # Find all BTC markets
41
45
  npx tsx scripts/info/search-markets.ts --query ETH --type perp # ETH perps only
42
46
  npx tsx scripts/info/search-markets.ts --query PURR --type spot # PURR spot only
47
+ npx tsx scripts/info/search-markets.ts --query HYPE --json # JSON output
43
48
  `);
44
49
  process.exit(0);
45
50
  } else if (!args.query && !arg.startsWith('-')) {
@@ -85,18 +90,23 @@ async function main() {
85
90
  client.verbose = args.verbose ?? false;
86
91
 
87
92
  const query = args.query.toUpperCase();
88
- console.log(`Searching for "${query}" across all markets...\n`);
93
+ if (!args.json) {
94
+ console.log(`Searching for "${query}" across all markets...\n`);
95
+ }
89
96
 
90
- const results: Array<{
97
+ interface Result {
91
98
  type: 'perp' | 'spot' | 'hip3';
92
99
  provider: string;
93
100
  coin: string;
101
+ assetId: number;
94
102
  price: string;
95
103
  volume24h: number;
96
104
  funding?: string;
97
105
  maxLeverage?: number;
98
106
  openInterest?: string;
99
- }> = [];
107
+ }
108
+
109
+ const results: Result[] = [];
100
110
 
101
111
  // Search main perps
102
112
  if (args.type === 'all' || args.type === 'perp') {
@@ -110,6 +120,7 @@ async function main() {
110
120
  type: 'perp',
111
121
  provider: 'Hyperliquid',
112
122
  coin: asset.name,
123
+ assetId: i,
113
124
  price: ctx.markPx,
114
125
  volume24h: parseFloat(ctx.dayNtlVlm),
115
126
  funding: ctx.funding,
@@ -126,7 +137,7 @@ async function main() {
126
137
  // On testnet, load specific dex if query is "dex:COIN" format
127
138
  if (args.query.includes(':')) {
128
139
  await client.loadSingleHip3Dex(args.query.split(':')[0]);
129
- } else {
140
+ } else if (!args.json) {
130
141
  console.log(' (Testnet: HIP-3 dexes not auto-loaded. Use "dexName:COIN" to search a specific dex.)\n');
131
142
  }
132
143
  }
@@ -143,10 +154,13 @@ async function main() {
143
154
  if (!asset || !ctx) continue;
144
155
 
145
156
  if (asset.name.toUpperCase().includes(query)) {
157
+ let assetId = -1;
158
+ try { assetId = client.getAssetIndex(asset.name); } catch { /* not registered */ }
146
159
  results.push({
147
160
  type: 'hip3',
148
161
  provider: dexData.dexName || `HIP-3 DEX ${dexIdx}`,
149
162
  coin: asset.name,
163
+ assetId,
150
164
  price: ctx.markPx,
151
165
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
152
166
  funding: ctx.funding,
@@ -199,6 +213,7 @@ async function main() {
199
213
  type: 'spot',
200
214
  provider: 'Spot',
201
215
  coin: displayName,
216
+ assetId: 10000 + pair.index,
202
217
  price: ctx.markPx,
203
218
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
204
219
  });
@@ -212,20 +227,25 @@ async function main() {
212
227
  // Sort by volume
213
228
  results.sort((a, b) => b.volume24h - a.volume24h);
214
229
 
230
+ if (args.json) {
231
+ console.log(JSON.stringify(results, null, 2));
232
+ return;
233
+ }
234
+
215
235
  if (results.length === 0) {
216
236
  console.log(`No markets found matching "${query}"`);
217
237
  return;
218
238
  }
219
239
 
220
240
  console.log(`Found ${results.length} market(s) matching "${query}":\n`);
221
- console.log('Type Provider Coin Price 24h Volume Funding (Ann.) OI');
222
- console.log('-'.repeat(100));
241
+ console.log('Type Provider Coin AssetID Price 24h Volume Funding (Ann.) OI');
242
+ console.log('-'.repeat(112));
223
243
 
224
244
  for (const m of results) {
225
245
  const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
226
246
  const oi = m.openInterest ? formatVolume(parseFloat(m.openInterest)) : '-';
227
247
  console.log(
228
- `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
248
+ `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
229
249
  );
230
250
  }
231
251
 
@@ -245,11 +265,11 @@ async function main() {
245
265
  const perpsWithFunding = markets.filter((m) => m.funding && m.type !== 'spot');
246
266
  if (perpsWithFunding.length > 1) {
247
267
  console.log(`\n=== ${coin} Funding Comparison ===\n`);
248
- console.log('Provider Coin Funding (Ann.) Price');
249
- console.log('-'.repeat(65));
268
+ console.log('Provider Coin AssetID Funding (Ann.) Price');
269
+ console.log('-'.repeat(78));
250
270
  for (const m of perpsWithFunding.sort((a, b) => parseFloat(b.funding!) - parseFloat(a.funding!))) {
251
271
  console.log(
252
- `${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${formatFunding(m.funding!).padStart(14)} ${formatPrice(m.price)}`
272
+ `${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatFunding(m.funding!).padStart(14)} ${formatPrice(m.price)}`
253
273
  );
254
274
  }
255
275
  }
@@ -9,6 +9,7 @@ interface Args {
9
9
  top?: number;
10
10
  verbose?: boolean;
11
11
  address?: string;
12
+ json?: boolean;
12
13
  }
13
14
 
14
15
  function parseArgs(): Args {
@@ -26,6 +27,8 @@ function parseArgs(): Args {
26
27
  args.address = process.argv[++i].toLowerCase();
27
28
  } else if (arg === '--verbose') {
28
29
  args.verbose = true;
30
+ } else if (arg === '--json') {
31
+ args.json = true;
29
32
  } else if (arg === '--help' || arg === '-h') {
30
33
  console.log(`
31
34
  Spot Markets - View Hyperliquid spot markets and balances
@@ -37,6 +40,7 @@ Options:
37
40
  --balances Show your spot token balances
38
41
  --address <0x...> Look up another account's spot balances (with --balances)
39
42
  --top <n> Show only top N markets by volume
43
+ --json Output as JSON (machine-readable)
40
44
  --verbose Show detailed output
41
45
  --help Show this help
42
46
 
@@ -85,10 +89,15 @@ async function main() {
85
89
  // Show balances
86
90
  if (args.balances) {
87
91
  const lookupAddress = args.address ?? client.address;
88
- console.log(`Fetching spot balances for ${lookupAddress}...\n`);
92
+ if (!args.json) console.log(`Fetching spot balances for ${lookupAddress}...\n`);
89
93
 
90
94
  const balances = await client.getSpotBalances(args.address);
91
95
 
96
+ if (args.json) {
97
+ console.log(JSON.stringify(balances.balances ?? [], null, 2));
98
+ return;
99
+ }
100
+
92
101
  if (!balances.balances || balances.balances.length === 0) {
93
102
  console.log('No spot token balances found.');
94
103
  return;
@@ -112,17 +121,20 @@ async function main() {
112
121
  }
113
122
 
114
123
  // Show markets
115
- console.log('Fetching spot market data...\n');
124
+ if (!args.json) console.log('Fetching spot market data...\n');
116
125
 
117
126
  const spotData = await client.getSpotMetaAndAssetCtxs();
118
127
 
119
128
  interface SpotMarket {
120
129
  name: string;
121
130
  index: number;
131
+ assetId: number;
122
132
  price: string;
123
133
  volume24h: number;
124
134
  change24h: string;
125
135
  tokens: [number, number];
136
+ base?: string;
137
+ quote?: string;
126
138
  }
127
139
 
128
140
  const markets: SpotMarket[] = [];
@@ -155,10 +167,13 @@ async function main() {
155
167
  markets.push({
156
168
  name: pair.name,
157
169
  index: pair.index,
170
+ assetId: 10000 + pair.index,
158
171
  price: ctx.markPx,
159
172
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
160
173
  change24h: formatChange(ctx.markPx, ctx.prevDayPx),
161
174
  tokens: pair.tokens,
175
+ base: tokenNameMap.get(pair.tokens[0]),
176
+ quote: tokenNameMap.get(pair.tokens[1]),
162
177
  });
163
178
  }
164
179
 
@@ -168,6 +183,11 @@ async function main() {
168
183
  // Apply top filter
169
184
  const displayMarkets = args.top ? markets.slice(0, args.top) : markets;
170
185
 
186
+ if (args.json) {
187
+ console.log(JSON.stringify(displayMarkets, null, 2));
188
+ return;
189
+ }
190
+
171
191
  if (displayMarkets.length === 0) {
172
192
  console.log(args.coin ? `No spot markets found for "${args.coin}"` : 'No spot markets found');
173
193
  return;
@@ -180,8 +200,8 @@ async function main() {
180
200
  }
181
201
 
182
202
  console.log(`=== Spot Markets (${displayMarkets.length} total) ===\n`);
183
- console.log('Pair Price 24h Volume 24h Change Base/Quote');
184
- console.log('-'.repeat(80));
203
+ console.log('Pair AssetID Price 24h Volume 24h Change Base/Quote');
204
+ console.log('-'.repeat(92));
185
205
 
186
206
  for (const m of displayMarkets) {
187
207
  const baseToken = tokenMap.get(m.tokens[0]);
@@ -189,7 +209,7 @@ async function main() {
189
209
  const pairStr = `${baseToken?.name || '?'}/${quoteToken?.name || '?'}`;
190
210
 
191
211
  console.log(
192
- `${m.name.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${m.change24h.padStart(11)} ${pairStr}`
212
+ `${m.name.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${m.change24h.padStart(11)} ${pairStr}`
193
213
  );
194
214
  }
195
215
 
@@ -11,6 +11,7 @@ Usage: openbroker trades --coin <symbol> [options]
11
11
  Options:
12
12
  --coin <symbol> Asset symbol (required, e.g. ETH, BTC)
13
13
  --top <n> Show last N trades (default: 30)
14
+ --json Output as JSON (machine-readable)
14
15
  --help, -h Show this help
15
16
 
16
17
  Examples:
@@ -35,10 +36,13 @@ async function main() {
35
36
  }
36
37
 
37
38
  const top = parseInt(args.top as string) || 30;
39
+ const jsonOutput = args.json as boolean;
38
40
  const client = getClient();
39
41
 
40
- console.log(`Open Broker - ${normalizeCoin(coin)} Recent Trades`);
41
- console.log('='.repeat(40) + '\n');
42
+ if (!jsonOutput) {
43
+ console.log(`Open Broker - ${normalizeCoin(coin)} Recent Trades`);
44
+ console.log('='.repeat(40) + '\n');
45
+ }
42
46
 
43
47
  try {
44
48
  // Load metadata (needed for HIP-3 coin resolution)
@@ -53,6 +57,13 @@ async function main() {
53
57
  trades.sort((a, b) => b.time - a.time);
54
58
  trades = trades.slice(0, top);
55
59
 
60
+ if (jsonOutput) {
61
+ let assetId = -1;
62
+ try { assetId = client.getAssetIndex(normalizeCoin(coin)); } catch { /* noop */ }
63
+ console.log(JSON.stringify({ coin: normalizeCoin(coin), assetId, trades }, null, 2));
64
+ return;
65
+ }
66
+
56
67
  if (trades.length === 0) {
57
68
  console.log('No recent trades found');
58
69
  return;