openbroker 1.0.47 → 1.0.48

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/CHANGELOG.md CHANGED
@@ -2,15 +2,16 @@
2
2
 
3
3
  All notable changes to Open Broker will be documented in this file.
4
4
 
5
- ## [1.0.47] - 2026-03-09
5
+ ## [1.0.48] - 2026-03-09
6
6
 
7
7
  ### Fixed
8
8
  - **HIP-3 Perp Trading**: All trading commands now work with HIP-3 assets using `dex:COIN` syntax (e.g., `--coin xyz:CL`)
9
9
  - `getMetaAndAssetCtxs()` loads HIP-3 dex assets into asset/szDecimals maps (asset index = `10000 * dexIdx + assetIdx`)
10
10
  - `getAllMids()` fetches and merges mid prices from all HIP-3 dexes
11
- - `getCandleSnapshot()`, `getFundingHistory()`, `getRecentTrades()`, `getL2Book()` pass correct `dex` param for HIP-3 coins
12
11
  - Market, limit, trigger, bracket, TWAP, scale, chase orders all work with HIP-3 assets
13
- - **HIP-3 Info Commands**: `candles`, `trades`, `funding-history` now return data for HIP-3 assets (previously returned null/empty)
12
+ - **HIP-3 Info Commands**: `funding`, `funding-history`, `candles`, `trades` now return data for HIP-3 assets (previously returned "No data" / null)
13
+ - **API Name Format**: Hyperliquid API returns HIP-3 names already prefixed (e.g., `xyz:CL`); fixed double-prefixing bug that caused all HIP-3 lookups to fail
14
+ - **Case Normalization**: Added `normalizeCoin()` helper — keeps dex prefix lowercase, uppercases asset (`xyz:cl` → `xyz:CL`). Fixes `toUpperCase()` mangling HIP-3 tickers to `XYZ:CL`
14
15
  - **Better Error Messages**: When a bare coin name (e.g., `CL`) matches HIP-3 assets, the error now suggests the prefixed ticker (e.g., `xyz:CL`)
15
16
 
16
17
  ### Added
package/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring. Exe
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.47", "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.48", "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_bracket ob_chase ob_watcher_status Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.47",
4
+ "version": "1.0.48",
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.47",
3
+ "version": "1.0.48",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -157,12 +157,15 @@ export class HyperliquidClient {
157
157
  this.log(`Loading HIP-3 dex: ${dex.name} with ${universe.length} markets`);
158
158
 
159
159
  universe.forEach((asset, assetIdx) => {
160
- const prefixedName = `${dex.name}:${asset.name}`;
160
+ // API returns names already prefixed (e.g., "xyz:CL"), use as-is
161
+ const coinName = asset.name;
162
+ // Extract local name by stripping dex prefix if present
163
+ const localName = coinName.startsWith(dex.name + ':') ? coinName.slice(dex.name.length + 1) : coinName;
161
164
  const globalIndex = 10000 * dexIdx + assetIdx;
162
165
 
163
- this.assetMap.set(prefixedName, globalIndex);
164
- this.szDecimalsMap.set(prefixedName, asset.szDecimals);
165
- this.coinDexMap.set(prefixedName, { dexName: dex.name, dexIdx, localName: asset.name });
166
+ this.assetMap.set(coinName, globalIndex);
167
+ this.szDecimalsMap.set(coinName, asset.szDecimals);
168
+ this.coinDexMap.set(coinName, { dexName: dex.name, dexIdx, localName });
166
169
  });
167
170
  }
168
171
  } catch (e) {
@@ -197,9 +200,9 @@ export class HyperliquidClient {
197
200
  });
198
201
  const dexMids = await dexResponse.json() as Record<string, string>;
199
202
 
200
- // Merge with prefixed keys
203
+ // Merge directly — API already returns prefixed keys (e.g., "xyz:CL")
201
204
  for (const [coin, mid] of Object.entries(dexMids)) {
202
- response[`${dex.name}:${coin}`] = mid;
205
+ response[coin] = mid;
203
206
  }
204
207
  } catch (e) {
205
208
  this.log(`Failed to fetch mids for HIP-3 dex ${dex.name}:`, e);
@@ -506,8 +509,8 @@ export class HyperliquidClient {
506
509
  spreadBps: number;
507
510
  }> {
508
511
  this.log('Fetching l2Book for:', coin);
509
- const localName = this.getCoinLocalName(coin);
510
- const response = await this.info.l2Book({ coin: localName });
512
+ // API accepts prefixed names directly (e.g., "xyz:CL")
513
+ const response = await this.info.l2Book({ coin });
511
514
 
512
515
  const bids = response.levels[0] as Array<{ px: string; sz: string; n: number }>;
513
516
  const asks = response.levels[1] as Array<{ px: string; sz: string; n: number }>;
@@ -954,12 +957,9 @@ export class HyperliquidClient {
954
957
  ? 'https://api.hyperliquid.xyz'
955
958
  : 'https://api.hyperliquid-testnet.xyz';
956
959
 
957
- const localName = this.getCoinLocalName(coin);
958
- const dex = this.getCoinDex(coin);
959
-
960
- const req: Record<string, unknown> = { coin: localName, interval, startTime };
960
+ // API accepts prefixed names directly (e.g., "xyz:CL")
961
+ const req: Record<string, unknown> = { coin, interval, startTime };
961
962
  if (endTime !== undefined) req.endTime = endTime;
962
- if (dex) req.dex = dex;
963
963
 
964
964
  const response = await fetch(baseUrl + '/info', {
965
965
  method: 'POST',
@@ -989,12 +989,9 @@ export class HyperliquidClient {
989
989
  ? 'https://api.hyperliquid.xyz'
990
990
  : 'https://api.hyperliquid-testnet.xyz';
991
991
 
992
- const localName = this.getCoinLocalName(coin);
993
- const dex = this.getCoinDex(coin);
994
-
995
- const body: Record<string, unknown> = { type: 'fundingHistory', coin: localName, startTime };
992
+ // API accepts prefixed names directly (e.g., "xyz:CL")
993
+ const body: Record<string, unknown> = { type: 'fundingHistory', coin, startTime };
996
994
  if (endTime !== undefined) body.endTime = endTime;
997
- if (dex) body.dex = dex;
998
995
 
999
996
  const response = await fetch(baseUrl + '/info', {
1000
997
  method: 'POST',
@@ -1023,11 +1020,8 @@ export class HyperliquidClient {
1023
1020
  ? 'https://api.hyperliquid.xyz'
1024
1021
  : 'https://api.hyperliquid-testnet.xyz';
1025
1022
 
1026
- const localName = this.getCoinLocalName(coin);
1027
- const dex = this.getCoinDex(coin);
1028
-
1029
- const body: Record<string, unknown> = { type: 'recentTrades', coin: localName };
1030
- if (dex) body.dex = dex;
1023
+ // API accepts prefixed names directly (e.g., "xyz:CL")
1024
+ const body: Record<string, unknown> = { type: 'recentTrades', coin };
1031
1025
 
1032
1026
  const response = await fetch(baseUrl + '/info', {
1033
1027
  method: 'POST',
@@ -119,6 +119,18 @@ export function parseArgs(args: string[]): Record<string, string | boolean> {
119
119
  return result;
120
120
  }
121
121
 
122
+ /**
123
+ * Normalize a coin name: uppercase the asset, keep dex prefix lowercase.
124
+ * "eth" → "ETH", "xyz:cl" → "xyz:CL", "xyz:CL" → "xyz:CL"
125
+ */
126
+ export function normalizeCoin(coin: string): string {
127
+ const colonIdx = coin.indexOf(':');
128
+ if (colonIdx >= 0) {
129
+ return coin.slice(0, colonIdx).toLowerCase() + ':' + coin.slice(colonIdx + 1).toUpperCase();
130
+ }
131
+ return coin.toUpperCase();
132
+ }
133
+
122
134
  /**
123
135
  * Sleep for specified milliseconds
124
136
  */
@@ -2,7 +2,7 @@
2
2
  // View OHLCV candle data from Hyperliquid
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatUsd, parseArgs } from '../core/utils.js';
5
+ import { formatUsd, parseArgs, normalizeCoin } from '../core/utils.js';
6
6
 
7
7
  const VALID_INTERVALS = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '3d', '1w', '1M'];
8
8
 
@@ -61,7 +61,7 @@ async function main() {
61
61
  const bars = parseInt(args.bars as string) || 24;
62
62
  const client = getClient();
63
63
 
64
- console.log(`Open Broker - ${coin.toUpperCase()} Candles (${interval})`);
64
+ console.log(`Open Broker - ${normalizeCoin(coin)} Candles (${interval})`);
65
65
  console.log('='.repeat(40) + '\n');
66
66
 
67
67
  try {
@@ -70,7 +70,7 @@ async function main() {
70
70
 
71
71
  const now = Date.now();
72
72
  const startTime = now - (bars * (INTERVAL_MS[interval] || 3_600_000));
73
- const candles = await client.getCandleSnapshot(coin.toUpperCase(), interval, startTime);
73
+ const candles = await client.getCandleSnapshot(normalizeCoin(coin), interval, startTime);
74
74
 
75
75
  if (candles.length === 0) {
76
76
  console.log('No candle data found');
@@ -2,7 +2,7 @@
2
2
  // View trade fill history from Hyperliquid
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatUsd, parseArgs } from '../core/utils.js';
5
+ import { formatUsd, parseArgs, normalizeCoin } from '../core/utils.js';
6
6
 
7
7
  function printUsage() {
8
8
  console.log(`
@@ -41,7 +41,7 @@ async function main() {
41
41
  let fills = await client.getUserFills();
42
42
 
43
43
  if (filterCoin) {
44
- fills = fills.filter(f => f.coin === filterCoin.toUpperCase());
44
+ fills = fills.filter(f => f.coin === normalizeCoin(filterCoin));
45
45
  }
46
46
  if (filterSide) {
47
47
  const sideCode = filterSide.toLowerCase() === 'buy' ? 'B' : 'A';
@@ -2,7 +2,7 @@
2
2
  // View historical funding rates for an asset on Hyperliquid
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatPercent, annualizeFundingRate, parseArgs } from '../core/utils.js';
5
+ import { formatPercent, annualizeFundingRate, parseArgs, normalizeCoin } from '../core/utils.js';
6
6
 
7
7
  function printUsage() {
8
8
  console.log(`
@@ -37,7 +37,7 @@ async function main() {
37
37
  const hours = parseInt(args.hours as string) || 24;
38
38
  const client = getClient();
39
39
 
40
- console.log(`Open Broker - ${coin.toUpperCase()} Funding History (${hours}h)`);
40
+ console.log(`Open Broker - ${normalizeCoin(coin)} Funding History (${hours}h)`);
41
41
  console.log('='.repeat(40) + '\n');
42
42
 
43
43
  try {
@@ -46,7 +46,7 @@ async function main() {
46
46
 
47
47
  const now = Date.now();
48
48
  const startTime = now - (hours * 3_600_000);
49
- const history = await client.getFundingHistory(coin.toUpperCase(), startTime);
49
+ const history = await client.getFundingHistory(normalizeCoin(coin), startTime);
50
50
 
51
51
  if (history.length === 0) {
52
52
  console.log('No funding history found');
@@ -67,7 +67,8 @@ async function scanFunding(client: ReturnType<typeof getClient>, options: {
67
67
  if (Math.abs(annualizedPct) < options.threshold) continue;
68
68
  if (openInterest < 100) continue;
69
69
 
70
- const coin = dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name;
70
+ // API returns HIP-3 names already prefixed (e.g., "xyz:CL")
71
+ const coin = asset.name;
71
72
 
72
73
  results.push({
73
74
  coin,
@@ -68,8 +68,9 @@ async function main() {
68
68
  const ctx = dexData.assetCtxs[i];
69
69
  if (!ctx) continue;
70
70
 
71
- const prefixedName = `${dexData.dexName}:${asset.name}`;
72
- if (filterCoin && prefixedName !== filterCoin) continue;
71
+ // API returns names already prefixed (e.g., "xyz:CL")
72
+ const coinName = asset.name;
73
+ if (filterCoin && coinName !== filterCoin) continue;
73
74
 
74
75
  const hourlyRate = parseFloat(ctx.funding);
75
76
  const annualizedRate = annualizeFundingRate(hourlyRate);
@@ -79,7 +80,7 @@ async function main() {
79
80
  if (!showAll && openInterest < 1000) continue;
80
81
 
81
82
  fundingData.push({
82
- coin: prefixedName,
83
+ coin: coinName,
83
84
  hourlyRate,
84
85
  annualizedRate,
85
86
  premium: 0,
@@ -71,8 +71,9 @@ async function main() {
71
71
  const ctx = dexData.assetCtxs[i];
72
72
  if (!ctx) continue;
73
73
 
74
- const prefixedName = `${dexData.dexName}:${asset.name}`;
75
- if (filterCoin && prefixedName !== filterCoin) continue;
74
+ // API returns names already prefixed (e.g., "xyz:CL")
75
+ const coinName = asset.name;
76
+ if (filterCoin && coinName !== filterCoin) continue;
76
77
 
77
78
  const markPx = parseFloat(ctx.markPx);
78
79
  const oraclePx = parseFloat(ctx.oraclePx);
@@ -82,7 +83,7 @@ async function main() {
82
83
  const change24h = prevDayPx > 0 ? (markPx - prevDayPx) / prevDayPx : 0;
83
84
 
84
85
  markets.push({
85
- coin: prefixedName,
86
+ coin: coinName,
86
87
  markPx,
87
88
  oraclePx,
88
89
  prevDayPx,
@@ -2,7 +2,7 @@
2
2
  // View historical orders from Hyperliquid
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatUsd, parseArgs } from '../core/utils.js';
5
+ import { formatUsd, parseArgs, normalizeCoin } from '../core/utils.js';
6
6
 
7
7
  function printUsage() {
8
8
  console.log(`
@@ -41,7 +41,7 @@ async function main() {
41
41
  let orders = await client.getHistoricalOrders();
42
42
 
43
43
  if (filterCoin) {
44
- orders = orders.filter(o => o.order.coin === filterCoin.toUpperCase());
44
+ orders = orders.filter(o => o.order.coin === normalizeCoin(filterCoin));
45
45
  }
46
46
  if (filterStatus) {
47
47
  const s = filterStatus.toLowerCase();
@@ -2,7 +2,7 @@
2
2
  // View recent trades (tape) for an asset on Hyperliquid
3
3
 
4
4
  import { getClient } from '../core/client.js';
5
- import { formatUsd, parseArgs } from '../core/utils.js';
5
+ import { formatUsd, parseArgs, normalizeCoin } from '../core/utils.js';
6
6
 
7
7
  function printUsage() {
8
8
  console.log(`
@@ -37,14 +37,14 @@ async function main() {
37
37
  const top = parseInt(args.top as string) || 30;
38
38
  const client = getClient();
39
39
 
40
- console.log(`Open Broker - ${coin.toUpperCase()} Recent Trades`);
40
+ console.log(`Open Broker - ${normalizeCoin(coin)} Recent Trades`);
41
41
  console.log('='.repeat(40) + '\n');
42
42
 
43
43
  try {
44
44
  // Load metadata (needed for HIP-3 coin resolution)
45
45
  await client.getMetaAndAssetCtxs();
46
46
 
47
- let trades = await client.getRecentTrades(coin.toUpperCase());
47
+ let trades = await client.getRecentTrades(normalizeCoin(coin));
48
48
 
49
49
  // Most recent first
50
50
  trades.sort((a, b) => b.time - a.time);
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { PluginTool } from './types.js';
5
5
  import type { PositionWatcher } from './watcher.js';
6
+ import { normalizeCoin } from '../core/utils.js';
6
7
 
7
8
  /** Helper to wrap a result as OpenClaw tool response */
8
9
  function json(payload: unknown) {
@@ -102,7 +103,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
102
103
  }));
103
104
 
104
105
  if (params.coin) {
105
- const coin = (params.coin as string).toUpperCase();
106
+ const coin = normalizeCoin(params.coin as string);
106
107
  positions = positions.filter(p => p.coin === coin);
107
108
  }
108
109
 
@@ -139,7 +140,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
139
140
  });
140
141
 
141
142
  if (params.coin) {
142
- const coin = (params.coin as string).toUpperCase();
143
+ const coin = normalizeCoin(params.coin as string);
143
144
  results = results.filter(r => r.coin === coin);
144
145
  }
145
146
 
@@ -181,7 +182,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
181
182
  }));
182
183
 
183
184
  if (params.coin) {
184
- const coin = (params.coin as string).toUpperCase();
185
+ const coin = normalizeCoin(params.coin as string);
185
186
  markets = markets.filter(m => m.coin === coin);
186
187
  }
187
188
 
@@ -238,7 +239,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
238
239
  const asset = dexData.meta.universe[i];
239
240
  if (asset.name.toUpperCase().includes(query)) {
240
241
  results.push({
241
- coin: `${dexData.dexName}:${asset.name}`,
242
+ // API returns names already prefixed (e.g., "xyz:CL")
243
+ coin: asset.name,
242
244
  type: 'hip3',
243
245
  dex: dexData.dexName,
244
246
  markPx: dexData.assetCtxs[i]?.markPx,
@@ -315,7 +317,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
315
317
  let fills = await client.getUserFills();
316
318
 
317
319
  if (params.coin) {
318
- const coin = (params.coin as string).toUpperCase();
320
+ const coin = normalizeCoin(params.coin as string);
319
321
  fills = fills.filter(f => f.coin === coin);
320
322
  }
321
323
  if (params.side) {
@@ -366,7 +368,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
366
368
  let orders = await client.getHistoricalOrders();
367
369
 
368
370
  if (params.coin) {
369
- const coin = (params.coin as string).toUpperCase();
371
+ const coin = normalizeCoin(params.coin as string);
370
372
  orders = orders.filter(o => o.order.coin === coin);
371
373
  }
372
374
  if (params.status) {
@@ -485,7 +487,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
485
487
  const { getClient } = await import('../core/client.js');
486
488
  const client = getClient();
487
489
 
488
- const coin = (params.coin as string).toUpperCase();
490
+ const coin = normalizeCoin(params.coin as string);
489
491
  const interval = (params.interval as string) || '1h';
490
492
  const bars = (params.bars as number) || 24;
491
493
 
@@ -533,7 +535,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
533
535
  const { annualizeFundingRate } = await import('../core/utils.js');
534
536
  const client = getClient();
535
537
 
536
- const coin = (params.coin as string).toUpperCase();
538
+ const coin = normalizeCoin(params.coin as string);
537
539
  const hours = (params.hours as number) || 24;
538
540
  const startTime = Date.now() - (hours * 3_600_000);
539
541
 
@@ -574,7 +576,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
574
576
  const { getClient } = await import('../core/client.js');
575
577
  const client = getClient();
576
578
 
577
- const coin = (params.coin as string).toUpperCase();
579
+ const coin = normalizeCoin(params.coin as string);
578
580
  // Load metadata for HIP-3 coin resolution
579
581
  await client.getMetaAndAssetCtxs();
580
582
  let trades = await client.getRecentTrades(coin);
@@ -673,7 +675,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
673
675
  if (openInterest < 100) continue;
674
676
 
675
677
  results.push({
676
- coin: dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name,
678
+ // API returns HIP-3 names already prefixed (e.g., "xyz:CL")
679
+ coin: asset.name,
677
680
  dex: dexData.dexName ?? 'main',
678
681
  annualizedPct: annualizedPct.toFixed(1),
679
682
  direction: hourlyRate > 0 ? 'longs pay shorts' : 'shorts pay longs',
@@ -742,7 +745,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
742
745
 
743
746
  if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
744
747
 
745
- const coin = (params.coin as string).toUpperCase();
748
+ const coin = normalizeCoin(params.coin as string);
746
749
  const size = params.size as number;
747
750
  const slippageBps = (params.slippage as number) ?? client.builderInfo.f;
748
751
 
@@ -791,7 +794,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
791
794
 
792
795
  if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
793
796
 
794
- const coin = (params.coin as string).toUpperCase();
797
+ const coin = normalizeCoin(params.coin as string);
795
798
  const size = params.size as number;
796
799
  const slippageBps = (params.slippage as number) ?? client.builderInfo.f;
797
800
 
@@ -843,7 +846,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
843
846
 
844
847
  if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
845
848
 
846
- const coin = (params.coin as string).toUpperCase();
849
+ const coin = normalizeCoin(params.coin as string);
847
850
  const isBuy = params.side === 'buy';
848
851
  const size = params.size as number;
849
852
  const price = params.price as number;
@@ -900,7 +903,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
900
903
 
901
904
  if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
902
905
 
903
- const coin = (params.coin as string).toUpperCase();
906
+ const coin = normalizeCoin(params.coin as string);
904
907
  const isBuy = params.side === 'buy';
905
908
  const size = params.size as number;
906
909
  const triggerPrice = params.trigger as number;
@@ -954,7 +957,7 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
954
957
  if (client.isReadOnly) return error('Wallet not configured. Run "openbroker setup" first.');
955
958
  if (!params.tp && !params.sl) return error('At least one of tp or sl is required.');
956
959
 
957
- const coin = (params.coin as string).toUpperCase();
960
+ const coin = normalizeCoin(params.coin as string);
958
961
 
959
962
  // Get current position
960
963
  const state = await client.getUserState();
@@ -1028,12 +1031,12 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
1028
1031
  if (params.oid) {
1029
1032
  const coin = params.coin as string | undefined;
1030
1033
  if (!coin) return error('--coin is required when cancelling by order ID');
1031
- const result = await client.cancel(coin.toUpperCase(), params.oid as number);
1032
- return json({ action: 'cancel', coin: coin.toUpperCase(), oid: params.oid, result });
1034
+ const result = await client.cancel(normalizeCoin(coin), params.oid as number);
1035
+ return json({ action: 'cancel', coin: normalizeCoin(coin), oid: params.oid, result });
1033
1036
  }
1034
1037
 
1035
1038
  if (params.all || params.coin) {
1036
- const coin = params.coin ? (params.coin as string).toUpperCase() : undefined;
1039
+ const coin = params.coin ? normalizeCoin(params.coin as string) : undefined;
1037
1040
  const results = await client.cancelAll(coin);
1038
1041
  return json({ action: 'cancelAll', coin: coin ?? 'all', results });
1039
1042
  }
@@ -105,13 +105,13 @@ async function main() {
105
105
  if (client.isHip3(coin)) {
106
106
  const allPerps = await client.getAllPerpMetas();
107
107
  const dexName = client.getCoinDex(coin);
108
- const localName = client.getCoinLocalName(coin);
109
108
  const dexData = allPerps.find(d => d.dexName === dexName);
110
109
  if (!dexData) {
111
110
  console.error(`Error: No market data for HIP-3 dex ${dexName}`);
112
111
  process.exit(1);
113
112
  }
114
- const assetIdx = dexData.meta.universe.findIndex(a => a.name === localName);
113
+ // API returns universe names already prefixed (e.g., "xyz:CL")
114
+ const assetIdx = dexData.meta.universe.findIndex(a => a.name === coin);
115
115
  if (assetIdx === -1 || !dexData.assetCtxs[assetIdx]) {
116
116
  console.error(`Error: No market data for ${coin}`);
117
117
  process.exit(1);
@@ -245,9 +245,9 @@ async function main() {
245
245
  if (client.isHip3(coin)) {
246
246
  const freshPerps = await client.getAllPerpMetas();
247
247
  const dexName = client.getCoinDex(coin);
248
- const localName = client.getCoinLocalName(coin);
249
248
  const dexData = freshPerps.find(d => d.dexName === dexName);
250
- const idx = dexData?.meta.universe.findIndex(a => a.name === localName) ?? -1;
249
+ // API returns universe names already prefixed (e.g., "xyz:CL")
250
+ const idx = dexData?.meta.universe.findIndex(a => a.name === coin) ?? -1;
251
251
  newHourlyFunding = idx >= 0 && dexData?.assetCtxs[idx] ? parseFloat(dexData.assetCtxs[idx].funding) : 0;
252
252
  } else {
253
253
  const newMeta = await client.getMetaAndAssetCtxs();