openbroker 1.0.44 → 1.0.47

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,6 +2,31 @@
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
6
+
7
+ ### Fixed
8
+ - **HIP-3 Perp Trading**: All trading commands now work with HIP-3 assets using `dex:COIN` syntax (e.g., `--coin xyz:CL`)
9
+ - `getMetaAndAssetCtxs()` loads HIP-3 dex assets into asset/szDecimals maps (asset index = `10000 * dexIdx + assetIdx`)
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
+ - 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)
14
+ - **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
+ ### Added
17
+ - **Funding Rate Scanner**: New `funding-scan` command for cross-dex funding rate scanning
18
+ - Scans all dexes (main + HIP-3) for high funding opportunities
19
+ - `--pairs` flag identifies opposing funding pairs for delta-neutral strategies
20
+ - `--watch --interval N` for periodic re-scanning
21
+ - `--json` output for piping to alerting systems
22
+ - `--main-only` / `--hip3-only` scope filters
23
+ - **HIP-3 Funding Rates**: `funding` and `markets` commands now support `--include-hip3` flag
24
+ - **HIP-3 Funding Arb**: `funding-arb` strategy now works with HIP-3 assets (monitoring loop correctly resolves HIP-3 funding data)
25
+ - **Plugin**: New `ob_funding_scan` agent tool for cross-dex funding scanning with pairs support
26
+ - **Plugin**: `ob_search` now searches HIP-3 perps in addition to main perps and spot
27
+ - **Client**: Added `getCoinDex()`, `getCoinLocalName()`, `isHip3()`, `getAllAssetNames()`, `getHip3AssetNames()`, `invalidateMetaCache()` methods
28
+ - **Client**: `getPerpDexs()` results are now cached to reduce redundant API calls
29
+
5
30
  ## [1.0.44] - 2026-02-25
6
31
 
7
32
  ### Added
package/README.md CHANGED
@@ -31,10 +31,15 @@ openbroker setup # One-command setup (wallet + config + builder a
31
31
  ```
32
32
 
33
33
  The setup command handles everything automatically:
34
- - Generates a new wallet or accepts your existing private key
34
+ - Generates a fresh trading wallet (recommended for agents) or accepts your existing private key
35
35
  - Saves configuration to `~/.openbroker/.env` (permissions `0600`)
36
36
  - Approves the builder fee (required for trading)
37
37
 
38
+ Setup offers three modes:
39
+ 1. **Generate fresh wallet** (recommended for agents) — cleanest option, no browser steps
40
+ 2. **Import existing key** — use a key you already have
41
+ 3. **Generate API wallet** — restricted wallet requiring browser approval from a master wallet
42
+
38
43
  ---
39
44
 
40
45
  ### Info Commands
@@ -636,16 +641,24 @@ export HYPERLIQUID_NETWORK=mainnet # Optional: mainnet (default) or testne
636
641
  export HYPERLIQUID_ACCOUNT_ADDRESS=0x... # Optional: for API wallets
637
642
  ```
638
643
 
639
- ### API Wallet Setup
644
+ ### Fresh Wallet Setup (Recommended for Agents)
645
+
646
+ The simplest setup for agents — generates a dedicated wallet, auto-approves the builder fee, and is ready to trade after funding:
647
+
648
+ ```bash
649
+ openbroker setup # Choose option 1, fund with USDC, start trading
650
+ ```
651
+
652
+ ### API Wallet Setup (Alternative)
640
653
 
641
- For automated trading, use an API wallet:
654
+ For delegated trading without moving funds, use an API wallet:
642
655
 
643
656
  ```bash
644
657
  export HYPERLIQUID_PRIVATE_KEY="0x..." # API wallet private key
645
658
  export HYPERLIQUID_ACCOUNT_ADDRESS="0x..." # Main account address
646
659
  ```
647
660
 
648
- **Note:** Builder fee must be approved with the main wallet first. Sub-accounts cannot approve builder fees. After approval, you can switch to using the API wallet for trading.
661
+ **Note:** API wallets require browser approval from the master wallet. The master wallet signs `ApproveAgent` and `ApproveBuilderFee` transactions via the approval URL provided during setup.
649
662
 
650
663
  ## Builder Fee
651
664
 
package/SKILL.md CHANGED
@@ -4,8 +4,8 @@ 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.44", "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
- 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_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status Bash(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)"}]}}
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
 
11
11
  # Open Broker - Hyperliquid Trading CLI
@@ -40,16 +40,26 @@ openbroker approve-builder --check # Check builder fee status (for troubleshoot
40
40
  ```
41
41
 
42
42
  The `setup` command offers three modes:
43
- 1. **Import existing key** — use a private key you already have (master wallet)
44
- 2. **Generate new wallet** — create a fresh master wallet
45
- 3. **Generate API wallet** (recommended for agents) — creates a restricted wallet that can trade but cannot withdraw
43
+ 1. **Generate fresh wallet** (recommended for agents) creates a dedicated trading wallet with builder fee auto-approved. No browser steps needed — just fund with USDC and start trading.
44
+ 2. **Import existing key** — use a private key you already have
45
+ 3. **Generate API wallet** — creates a restricted wallet that can trade but cannot withdraw. Requires browser approval from a master wallet.
46
46
 
47
47
  For options 1 and 2, setup saves config and approves the builder fee automatically.
48
48
  For option 3 (API wallet), see the API Wallet Setup section below.
49
49
 
50
- ### API Wallet Setup (Recommended for Agents)
50
+ ### Fresh Wallet Setup (Recommended for Agents)
51
51
 
52
- API wallets can place trades on behalf of a master account but **cannot withdraw funds**. This is the safest option for automated agents.
52
+ The simplest setup for agents. A fresh wallet is generated, the builder fee is auto-approved, and the agent is ready to trade immediately after funding.
53
+
54
+ **Flow:**
55
+ 1. Run `openbroker setup` and choose option 1 ("Generate a fresh wallet")
56
+ 2. The CLI generates a wallet, saves the config, and approves the builder fee automatically
57
+ 3. Fund the wallet with USDC on Arbitrum, then deposit at https://app.hyperliquid.xyz/
58
+ 4. Start trading
59
+
60
+ ### API Wallet Setup (Alternative)
61
+
62
+ API wallets can place trades on behalf of a master account but **cannot withdraw funds**. Use this if you prefer to keep funds in your existing wallet and only delegate trading access.
53
63
 
54
64
  **Flow:**
55
65
  1. Run `openbroker setup` and choose option 3 ("Generate API wallet")
@@ -160,8 +170,25 @@ openbroker trades --coin BTC --top 50 # Last 50 trades
160
170
  openbroker rate-limit # API usage and capacity
161
171
  ```
162
172
 
173
+ ### Funding Rate Scanner (Cross-Dex)
174
+ ```bash
175
+ openbroker funding-scan # Scan all dexes, >25% threshold
176
+ openbroker funding-scan --threshold 50 --pairs # Show opposing funding pairs
177
+ openbroker funding-scan --hip3-only --top 20 # HIP-3 only
178
+ openbroker funding-scan --watch --interval 120 # Re-scan every 2 minutes
179
+ ```
180
+
163
181
  ## Trading Commands
164
182
 
183
+ ### HIP-3 Perp Trading
184
+ All trading commands support HIP-3 assets using `dex:COIN` syntax:
185
+ ```bash
186
+ openbroker buy --coin xyz:CL --size 1 # Buy crude oil on xyz dex
187
+ openbroker sell --coin xyz:BRENTOIL --size 1 # Sell brent oil
188
+ openbroker limit --coin xyz:GOLD --side buy --size 0.1 --price 2500
189
+ openbroker funding-arb --coin xyz:CL --size 5000 # Funding arb on HIP-3
190
+ ```
191
+
165
192
  ### Market Orders (Quick)
166
193
  ```bash
167
194
  openbroker buy --coin ETH --size 0.1
package/bin/cli.ts CHANGED
@@ -31,6 +31,7 @@ const commands: Record<string, { script: string; description: string }> = {
31
31
  'funding-history': { script: 'info/funding-history.ts', description: 'View historical funding rates' },
32
32
  'trades': { script: 'info/trades.ts', description: 'View recent trades for an asset' },
33
33
  'rate-limit': { script: 'info/rate-limit.ts', description: 'View API rate limit status' },
34
+ 'funding-scan': { script: 'info/funding-scan.ts', description: 'Scan funding rates across all dexes' },
34
35
 
35
36
  // Operations
36
37
  'buy': { script: 'operations/market-order.ts', description: 'Market buy order' },
@@ -78,6 +79,7 @@ Info Commands:
78
79
  search Search for assets across all providers
79
80
  spot View spot markets and balances
80
81
  rate-limit View API rate limit status
82
+ funding-scan Scan funding rates across all dexes (main + HIP-3)
81
83
 
82
84
  Trading Commands:
83
85
  buy Market buy order
@@ -148,7 +150,7 @@ function runScript(scriptPath: string, args: string[]) {
148
150
  function main() {
149
151
  const args = process.argv.slice(2);
150
152
 
151
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
153
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
152
154
  printHelp();
153
155
  process.exit(0);
154
156
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.44",
4
+ "version": "1.0.47",
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.44",
3
+ "version": "1.0.47",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "scale": "tsx scripts/operations/scale.ts",
39
39
  "bracket": "tsx scripts/operations/bracket.ts",
40
40
  "chase": "tsx scripts/operations/chase.ts",
41
+ "funding-scan": "tsx scripts/info/funding-scan.ts",
41
42
  "funding-arb": "tsx scripts/strategies/funding-arb.ts",
42
43
  "grid": "tsx scripts/strategies/grid.ts",
43
44
  "dca": "tsx scripts/strategies/dca.ts",
@@ -25,6 +25,12 @@ export class HyperliquidClient {
25
25
  private meta: MetaAndAssetCtxs | null = null;
26
26
  private assetMap: Map<string, number> = new Map();
27
27
  private szDecimalsMap: Map<string, number> = new Map();
28
+ /** Maps coin name → dex info for HIP-3 assets. Main dex assets have dexName=null */
29
+ private coinDexMap: Map<string, { dexName: string | null; dexIdx: number; localName: string }> = new Map();
30
+ /** Cache of perpDexs list */
31
+ private perpDexsCache: Array<{ name: string; fullName: string; deployer: string } | null> | null = null;
32
+ /** Whether HIP-3 assets have been loaded into maps */
33
+ private hip3Loaded: boolean = false;
28
34
  public verbose: boolean = false;
29
35
 
30
36
  constructor(config?: OpenBrokerConfig) {
@@ -106,18 +112,103 @@ export class HyperliquidClient {
106
112
  assetCtxs: response[1],
107
113
  };
108
114
 
109
- // Build lookup maps
115
+ // Build lookup maps for main dex
110
116
  this.meta.meta.universe.forEach((asset, index) => {
111
117
  this.assetMap.set(asset.name, index);
112
118
  this.szDecimalsMap.set(asset.name, asset.szDecimals);
119
+ this.coinDexMap.set(asset.name, { dexName: null, dexIdx: 0, localName: asset.name });
113
120
  });
114
121
 
122
+ // Load HIP-3 dex assets (only once - maps persist across meta cache invalidation)
123
+ if (!this.hip3Loaded) {
124
+ await this.loadHip3Assets();
125
+ this.hip3Loaded = true;
126
+ }
127
+
115
128
  return this.meta;
116
129
  }
117
130
 
131
+ /**
132
+ * Load HIP-3 perp dex assets into the asset/szDecimals maps.
133
+ * Asset index formula: 10000 * dexIdx + assetIdx
134
+ * Coins are keyed as "dexName:COIN" (e.g., "xyz:CL")
135
+ */
136
+ private async loadHip3Assets(): Promise<void> {
137
+ try {
138
+ const dexs = await this.getPerpDexs();
139
+ const baseUrl = isMainnet()
140
+ ? 'https://api.hyperliquid.xyz'
141
+ : 'https://api.hyperliquid-testnet.xyz';
142
+
143
+ for (let dexIdx = 1; dexIdx < dexs.length; dexIdx++) {
144
+ const dex = dexs[dexIdx];
145
+ if (!dex) continue;
146
+
147
+ try {
148
+ const dexResponse = await fetch(baseUrl + '/info', {
149
+ method: 'POST',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ type: 'metaAndAssetCtxs', dex: dex.name }),
152
+ });
153
+ const dexData = await dexResponse.json();
154
+
155
+ if (dexData && dexData[0]?.universe) {
156
+ const universe = dexData[0].universe as Array<{ name: string; szDecimals: number; maxLeverage: number; onlyIsolated?: boolean }>;
157
+ this.log(`Loading HIP-3 dex: ${dex.name} with ${universe.length} markets`);
158
+
159
+ universe.forEach((asset, assetIdx) => {
160
+ const prefixedName = `${dex.name}:${asset.name}`;
161
+ const globalIndex = 10000 * dexIdx + assetIdx;
162
+
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
+ });
167
+ }
168
+ } catch (e) {
169
+ this.log(`Failed to load HIP-3 dex ${dex.name}:`, e);
170
+ }
171
+ }
172
+ } catch (e) {
173
+ this.log('Failed to load HIP-3 assets:', e);
174
+ }
175
+ }
176
+
118
177
  async getAllMids(): Promise<Record<string, string>> {
119
178
  this.log('Fetching allMids...');
120
- const response = await this.info.allMids();
179
+ const response = await this.info.allMids() as Record<string, string>;
180
+
181
+ // Also fetch HIP-3 dex mids
182
+ try {
183
+ const dexs = await this.getPerpDexs();
184
+ const baseUrl = isMainnet()
185
+ ? 'https://api.hyperliquid.xyz'
186
+ : 'https://api.hyperliquid-testnet.xyz';
187
+
188
+ for (let i = 1; i < dexs.length; i++) {
189
+ const dex = dexs[i];
190
+ if (!dex) continue;
191
+
192
+ try {
193
+ const dexResponse = await fetch(baseUrl + '/info', {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({ type: 'allMids', dex: dex.name }),
197
+ });
198
+ const dexMids = await dexResponse.json() as Record<string, string>;
199
+
200
+ // Merge with prefixed keys
201
+ for (const [coin, mid] of Object.entries(dexMids)) {
202
+ response[`${dex.name}:${coin}`] = mid;
203
+ }
204
+ } catch (e) {
205
+ this.log(`Failed to fetch mids for HIP-3 dex ${dex.name}:`, e);
206
+ }
207
+ }
208
+ } catch (e) {
209
+ this.log('Failed to fetch HIP-3 mids:', e);
210
+ }
211
+
121
212
  return response;
122
213
  }
123
214
 
@@ -130,6 +221,8 @@ export class HyperliquidClient {
130
221
  fullName: string;
131
222
  deployer: string;
132
223
  } | null>> {
224
+ if (this.perpDexsCache) return this.perpDexsCache;
225
+
133
226
  this.log('Fetching perpDexs...');
134
227
  const baseUrl = isMainnet()
135
228
  ? 'https://api.hyperliquid.xyz'
@@ -142,6 +235,7 @@ export class HyperliquidClient {
142
235
  });
143
236
  const data = await response.json();
144
237
  this.log('perpDexs response:', JSON.stringify(data).slice(0, 500));
238
+ this.perpDexsCache = data;
145
239
  return data;
146
240
  }
147
241
 
@@ -412,7 +506,8 @@ export class HyperliquidClient {
412
506
  spreadBps: number;
413
507
  }> {
414
508
  this.log('Fetching l2Book for:', coin);
415
- const response = await this.info.l2Book({ coin });
509
+ const localName = this.getCoinLocalName(coin);
510
+ const response = await this.info.l2Book({ coin: localName });
416
511
 
417
512
  const bids = response.levels[0] as Array<{ px: string; sz: string; n: number }>;
418
513
  const asks = response.levels[1] as Array<{ px: string; sz: string; n: number }>;
@@ -437,6 +532,15 @@ export class HyperliquidClient {
437
532
  getAssetIndex(coin: string): number {
438
533
  const index = this.assetMap.get(coin);
439
534
  if (index === undefined) {
535
+ // Check if bare name exists in HIP-3 dexes and suggest prefixed version
536
+ const hip3Matches = this.findHip3Matches(coin);
537
+ if (hip3Matches.length > 0) {
538
+ const suggestions = hip3Matches.map(m => `${m}`).join(', ');
539
+ throw new Error(
540
+ `Unknown asset: ${coin}. Did you mean one of these HIP-3 assets? ${suggestions}\n` +
541
+ `Use "openbroker search --query ${coin}" to find the full ticker.`
542
+ );
543
+ }
440
544
  throw new Error(`Unknown asset: ${coin}. Available: ${Array.from(this.assetMap.keys()).slice(0, 10).join(', ')}...`);
441
545
  }
442
546
  return index;
@@ -445,11 +549,78 @@ export class HyperliquidClient {
445
549
  getSzDecimals(coin: string): number {
446
550
  const decimals = this.szDecimalsMap.get(coin);
447
551
  if (decimals === undefined) {
552
+ const hip3Matches = this.findHip3Matches(coin);
553
+ if (hip3Matches.length > 0) {
554
+ throw new Error(
555
+ `Unknown asset: ${coin}. Did you mean: ${hip3Matches.join(', ')}?`
556
+ );
557
+ }
448
558
  throw new Error(`Unknown asset: ${coin}`);
449
559
  }
450
560
  return decimals;
451
561
  }
452
562
 
563
+ /**
564
+ * Find HIP-3 assets matching a bare coin name (without dex prefix)
565
+ */
566
+ private findHip3Matches(bareName: string): string[] {
567
+ const matches: string[] = [];
568
+ const upperName = bareName.toUpperCase();
569
+ for (const [key, info] of this.coinDexMap.entries()) {
570
+ if (info.dexName && info.localName.toUpperCase() === upperName) {
571
+ matches.push(key);
572
+ }
573
+ }
574
+ return matches;
575
+ }
576
+
577
+ /**
578
+ * Get the dex name for a coin (null for main dex assets)
579
+ */
580
+ getCoinDex(coin: string): string | null {
581
+ return this.coinDexMap.get(coin)?.dexName ?? null;
582
+ }
583
+
584
+ /**
585
+ * Get the local (unprefixed) coin name for API calls that need it
586
+ * e.g., "xyz:CL" → "CL", "ETH" → "ETH"
587
+ */
588
+ getCoinLocalName(coin: string): string {
589
+ return this.coinDexMap.get(coin)?.localName ?? coin;
590
+ }
591
+
592
+ /**
593
+ * Check if a coin is a HIP-3 asset
594
+ */
595
+ isHip3(coin: string): boolean {
596
+ return this.coinDexMap.get(coin)?.dexName != null;
597
+ }
598
+
599
+ /**
600
+ * Invalidate cached metadata so next call fetches fresh data.
601
+ * Useful for long-running strategies that need updated funding rates.
602
+ */
603
+ invalidateMetaCache(): void {
604
+ this.meta = null;
605
+ // Keep the asset/szDecimals/coinDex maps - they don't change
606
+ }
607
+
608
+ /**
609
+ * Get all loaded asset names (main + HIP-3)
610
+ */
611
+ getAllAssetNames(): string[] {
612
+ return Array.from(this.assetMap.keys());
613
+ }
614
+
615
+ /**
616
+ * Get all HIP-3 asset names
617
+ */
618
+ getHip3AssetNames(): string[] {
619
+ return Array.from(this.coinDexMap.entries())
620
+ .filter(([_, info]) => info.dexName !== null)
621
+ .map(([name]) => name);
622
+ }
623
+
453
624
  // ============ Account Info ============
454
625
 
455
626
  /**
@@ -783,8 +954,12 @@ export class HyperliquidClient {
783
954
  ? 'https://api.hyperliquid.xyz'
784
955
  : 'https://api.hyperliquid-testnet.xyz';
785
956
 
786
- const req: Record<string, unknown> = { coin, interval, startTime };
957
+ const localName = this.getCoinLocalName(coin);
958
+ const dex = this.getCoinDex(coin);
959
+
960
+ const req: Record<string, unknown> = { coin: localName, interval, startTime };
787
961
  if (endTime !== undefined) req.endTime = endTime;
962
+ if (dex) req.dex = dex;
788
963
 
789
964
  const response = await fetch(baseUrl + '/info', {
790
965
  method: 'POST',
@@ -814,8 +989,12 @@ export class HyperliquidClient {
814
989
  ? 'https://api.hyperliquid.xyz'
815
990
  : 'https://api.hyperliquid-testnet.xyz';
816
991
 
817
- const body: Record<string, unknown> = { type: 'fundingHistory', coin, startTime };
992
+ const localName = this.getCoinLocalName(coin);
993
+ const dex = this.getCoinDex(coin);
994
+
995
+ const body: Record<string, unknown> = { type: 'fundingHistory', coin: localName, startTime };
818
996
  if (endTime !== undefined) body.endTime = endTime;
997
+ if (dex) body.dex = dex;
819
998
 
820
999
  const response = await fetch(baseUrl + '/info', {
821
1000
  method: 'POST',
@@ -844,10 +1023,16 @@ export class HyperliquidClient {
844
1023
  ? 'https://api.hyperliquid.xyz'
845
1024
  : 'https://api.hyperliquid-testnet.xyz';
846
1025
 
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;
1031
+
847
1032
  const response = await fetch(baseUrl + '/info', {
848
1033
  method: 'POST',
849
1034
  headers: { 'Content-Type': 'application/json' },
850
- body: JSON.stringify({ type: 'recentTrades', coin }),
1035
+ body: JSON.stringify(body),
851
1036
  });
852
1037
  const data = await response.json();
853
1038
  this.log('recentTrades response length:', data?.length);
@@ -65,6 +65,9 @@ async function main() {
65
65
  console.log('='.repeat(40) + '\n');
66
66
 
67
67
  try {
68
+ // Load metadata (needed for HIP-3 coin resolution)
69
+ await client.getMetaAndAssetCtxs();
70
+
68
71
  const now = Date.now();
69
72
  const startTime = now - (bars * (INTERVAL_MS[interval] || 3_600_000));
70
73
  const candles = await client.getCandleSnapshot(coin.toUpperCase(), interval, startTime);
@@ -41,6 +41,9 @@ async function main() {
41
41
  console.log('='.repeat(40) + '\n');
42
42
 
43
43
  try {
44
+ // Load metadata (needed for HIP-3 coin resolution)
45
+ await client.getMetaAndAssetCtxs();
46
+
44
47
  const now = Date.now();
45
48
  const startTime = now - (hours * 3_600_000);
46
49
  const history = await client.getFundingHistory(coin.toUpperCase(), startTime);
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Cross-dex funding rate scanner - scan all assets (main + HIP-3) for funding opportunities
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { formatUsd, formatPercent, annualizeFundingRate, parseArgs, sleep } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Usage: openbroker funding-scan [options]
10
+
11
+ Scan funding rates across all dexes (main perps + HIP-3) and find opportunities.
12
+
13
+ Options:
14
+ --threshold <n> Min annualized funding rate % to show (default: 25)
15
+ --include-hip3 Include HIP-3 dex assets (default: true)
16
+ --main-only Only scan main perps
17
+ --hip3-only Only scan HIP-3 perps
18
+ --top <n> Show top N results (default: 30)
19
+ --pairs Show correlated pairs with opposing funding
20
+ --json Output as JSON
21
+ --watch Re-scan periodically
22
+ --interval <n> Watch interval in seconds (default: 60)
23
+ --help, -h Show this help
24
+
25
+ Examples:
26
+ openbroker funding-scan
27
+ openbroker funding-scan --threshold 50 --pairs
28
+ openbroker funding-scan --hip3-only --top 20
29
+ openbroker funding-scan --watch --interval 120
30
+ `);
31
+ }
32
+
33
+ interface FundingScanResult {
34
+ coin: string;
35
+ dex: string;
36
+ hourlyRate: number;
37
+ annualizedPct: number;
38
+ direction: 'longs pay' | 'shorts pay';
39
+ openInterest: number;
40
+ markPx: number;
41
+ }
42
+
43
+ async function scanFunding(client: ReturnType<typeof getClient>, options: {
44
+ threshold: number;
45
+ mainOnly: boolean;
46
+ hip3Only: boolean;
47
+ topN: number;
48
+ }): Promise<FundingScanResult[]> {
49
+ const allPerps = await client.getAllPerpMetas();
50
+ const results: FundingScanResult[] = [];
51
+
52
+ for (const dexData of allPerps) {
53
+ const isMain = !dexData.dexName;
54
+ if (options.mainOnly && !isMain) continue;
55
+ if (options.hip3Only && isMain) continue;
56
+
57
+ for (let i = 0; i < dexData.meta.universe.length; i++) {
58
+ const asset = dexData.meta.universe[i];
59
+ const ctx = dexData.assetCtxs[i];
60
+ if (!ctx) continue;
61
+
62
+ const hourlyRate = parseFloat(ctx.funding);
63
+ const annualizedPct = annualizeFundingRate(hourlyRate) * 100;
64
+ const openInterest = parseFloat(ctx.openInterest);
65
+ const markPx = parseFloat(ctx.markPx);
66
+
67
+ if (Math.abs(annualizedPct) < options.threshold) continue;
68
+ if (openInterest < 100) continue;
69
+
70
+ const coin = dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name;
71
+
72
+ results.push({
73
+ coin,
74
+ dex: dexData.dexName ?? 'main',
75
+ hourlyRate,
76
+ annualizedPct,
77
+ direction: hourlyRate > 0 ? 'longs pay' : 'shorts pay',
78
+ openInterest,
79
+ markPx,
80
+ });
81
+ }
82
+ }
83
+
84
+ // Sort by absolute annualized rate
85
+ results.sort((a, b) => Math.abs(b.annualizedPct) - Math.abs(a.annualizedPct));
86
+ return results.slice(0, options.topN);
87
+ }
88
+
89
+ function printResults(results: FundingScanResult[], showPairs: boolean) {
90
+ if (results.length === 0) {
91
+ console.log('No assets above threshold');
92
+ return;
93
+ }
94
+
95
+ // Table header
96
+ console.log(
97
+ 'Coin'.padEnd(16) +
98
+ 'Dex'.padEnd(8) +
99
+ 'Annualized'.padEnd(14) +
100
+ 'Direction'.padEnd(14) +
101
+ 'OI'.padEnd(14) +
102
+ 'Mark'
103
+ );
104
+ console.log('─'.repeat(75));
105
+
106
+ for (const r of results) {
107
+ const annStr = `${r.annualizedPct >= 0 ? '+' : ''}${r.annualizedPct.toFixed(1)}%`;
108
+ const oiStr = formatOI(r.openInterest);
109
+
110
+ console.log(
111
+ r.coin.padEnd(16) +
112
+ r.dex.padEnd(8) +
113
+ annStr.padStart(12).padEnd(14) +
114
+ r.direction.padEnd(14) +
115
+ oiStr.padStart(12).padEnd(14) +
116
+ formatUsd(r.markPx)
117
+ );
118
+ }
119
+
120
+ // Show opposing pairs
121
+ if (showPairs) {
122
+ console.log('\nOpposing Funding Pairs:');
123
+ console.log('─'.repeat(75));
124
+
125
+ const longs = results.filter(r => r.annualizedPct > 0); // longs pay shorts
126
+ const shorts = results.filter(r => r.annualizedPct < 0); // shorts pay longs
127
+
128
+ const pairs: Array<{ long: FundingScanResult; short: FundingScanResult; spread: number }> = [];
129
+
130
+ for (const l of longs) {
131
+ for (const s of shorts) {
132
+ // Only pair across different dexes or correlated assets
133
+ const spread = l.annualizedPct + Math.abs(s.annualizedPct);
134
+ if (spread > 20) {
135
+ pairs.push({ long: l, short: s, spread });
136
+ }
137
+ }
138
+ }
139
+
140
+ pairs.sort((a, b) => b.spread - a.spread);
141
+
142
+ for (const p of pairs.slice(0, 10)) {
143
+ console.log(
144
+ ` SHORT ${p.long.coin.padEnd(14)} (${p.long.annualizedPct.toFixed(1)}%) ` +
145
+ `+ LONG ${p.short.coin.padEnd(14)} (${p.short.annualizedPct.toFixed(1)}%) ` +
146
+ `= ${p.spread.toFixed(1)}% spread`
147
+ );
148
+ }
149
+
150
+ if (pairs.length === 0) {
151
+ console.log(' No strong opposing pairs found');
152
+ }
153
+ }
154
+ }
155
+
156
+ function formatOI(oi: number): string {
157
+ if (oi >= 1_000_000) return `$${(oi / 1_000_000).toFixed(2)}M`;
158
+ if (oi >= 1_000) return `$${(oi / 1_000).toFixed(1)}K`;
159
+ return `$${oi.toFixed(0)}`;
160
+ }
161
+
162
+ async function main() {
163
+ const args = parseArgs(process.argv.slice(2));
164
+
165
+ if (args.help || args.h) {
166
+ printUsage();
167
+ process.exit(0);
168
+ }
169
+
170
+ const threshold = args.threshold ? parseFloat(args.threshold as string) : 25;
171
+ const mainOnly = args['main-only'] as boolean ?? false;
172
+ const hip3Only = args['hip3-only'] as boolean ?? false;
173
+ const topN = parseInt(args.top as string) || 30;
174
+ const showPairs = args.pairs as boolean ?? false;
175
+ const outputJson = args.json as boolean ?? false;
176
+ const watch = args.watch as boolean ?? false;
177
+ const intervalSec = args.interval ? parseInt(args.interval as string) : 60;
178
+
179
+ const client = getClient();
180
+ if (args.verbose) client.verbose = true;
181
+
182
+ console.log('Open Broker - Funding Rate Scanner');
183
+ console.log('==================================\n');
184
+ console.log(`Threshold: ${threshold}% annualized | Scope: ${mainOnly ? 'main only' : hip3Only ? 'HIP-3 only' : 'all dexes'}\n`);
185
+
186
+ const options = { threshold, mainOnly, hip3Only, topN };
187
+
188
+ const runScan = async () => {
189
+ const results = await scanFunding(client, options);
190
+
191
+ if (outputJson) {
192
+ console.log(JSON.stringify(results, null, 2));
193
+ } else {
194
+ printResults(results, showPairs);
195
+ }
196
+
197
+ return results;
198
+ };
199
+
200
+ await runScan();
201
+
202
+ if (watch) {
203
+ console.log(`\nWatching every ${intervalSec}s... (Ctrl+C to stop)\n`);
204
+ while (true) {
205
+ await sleep(intervalSec * 1000);
206
+ console.log(`\n[${new Date().toLocaleTimeString()}] Rescanning...\n`);
207
+ await runScan();
208
+ }
209
+ }
210
+ }
211
+
212
+ main().catch(err => { console.error(err); process.exit(1); });
@@ -24,12 +24,14 @@ async function main() {
24
24
  console.log('===========================\n');
25
25
 
26
26
  const client = getClient();
27
+ const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
27
28
 
28
29
  try {
29
30
  const meta = await client.getMetaAndAssetCtxs();
30
31
 
31
32
  const fundingData: FundingDisplay[] = [];
32
33
 
34
+ // Main dex assets
33
35
  for (let i = 0; i < meta.meta.universe.length; i++) {
34
36
  const asset = meta.meta.universe[i];
35
37
  const ctx = meta.assetCtxs[i];
@@ -55,6 +57,39 @@ async function main() {
55
57
  });
56
58
  }
57
59
 
60
+ // Include HIP-3 dex assets
61
+ if (includeHip3 || showAll || (filterCoin && filterCoin.includes(':'))) {
62
+ const allPerps = await client.getAllPerpMetas();
63
+ for (const dexData of allPerps) {
64
+ if (!dexData.dexName) continue; // Skip main dex (already loaded)
65
+
66
+ for (let i = 0; i < dexData.meta.universe.length; i++) {
67
+ const asset = dexData.meta.universe[i];
68
+ const ctx = dexData.assetCtxs[i];
69
+ if (!ctx) continue;
70
+
71
+ const prefixedName = `${dexData.dexName}:${asset.name}`;
72
+ if (filterCoin && prefixedName !== filterCoin) continue;
73
+
74
+ const hourlyRate = parseFloat(ctx.funding);
75
+ const annualizedRate = annualizeFundingRate(hourlyRate);
76
+ const openInterest = parseFloat(ctx.openInterest);
77
+ const markPx = parseFloat(ctx.markPx);
78
+
79
+ if (!showAll && openInterest < 1000) continue;
80
+
81
+ fundingData.push({
82
+ coin: prefixedName,
83
+ hourlyRate,
84
+ annualizedRate,
85
+ premium: 0,
86
+ openInterest,
87
+ markPx,
88
+ });
89
+ }
90
+ }
91
+ }
92
+
58
93
  // Sort
59
94
  if (sortBy === 'hourly' || sortBy === 'annualized') {
60
95
  fundingData.sort((a, b) => Math.abs(b.annualizedRate) - Math.abs(a.annualizedRate));
@@ -26,6 +26,7 @@ async function main() {
26
26
  console.log('=====================\n');
27
27
 
28
28
  const client = getClient();
29
+ const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
29
30
 
30
31
  try {
31
32
  const meta = await client.getMetaAndAssetCtxs();
@@ -59,6 +60,42 @@ async function main() {
59
60
  });
60
61
  }
61
62
 
63
+ // Include HIP-3 markets
64
+ if (includeHip3 || (filterCoin && filterCoin.includes(':'))) {
65
+ const allPerps = await client.getAllPerpMetas();
66
+ for (const dexData of allPerps) {
67
+ if (!dexData.dexName) continue;
68
+
69
+ for (let i = 0; i < dexData.meta.universe.length; i++) {
70
+ const asset = dexData.meta.universe[i];
71
+ const ctx = dexData.assetCtxs[i];
72
+ if (!ctx) continue;
73
+
74
+ const prefixedName = `${dexData.dexName}:${asset.name}`;
75
+ if (filterCoin && prefixedName !== filterCoin) continue;
76
+
77
+ const markPx = parseFloat(ctx.markPx);
78
+ const oraclePx = parseFloat(ctx.oraclePx);
79
+ const prevDayPx = parseFloat(ctx.prevDayPx);
80
+ const volume24h = parseFloat(ctx.dayNtlVlm);
81
+ const openInterest = parseFloat(ctx.openInterest);
82
+ const change24h = prevDayPx > 0 ? (markPx - prevDayPx) / prevDayPx : 0;
83
+
84
+ markets.push({
85
+ coin: prefixedName,
86
+ markPx,
87
+ oraclePx,
88
+ prevDayPx,
89
+ change24h,
90
+ volume24h,
91
+ openInterest,
92
+ maxLeverage: asset.maxLeverage,
93
+ szDecimals: asset.szDecimals,
94
+ });
95
+ }
96
+ }
97
+ }
98
+
62
99
  // Sort
63
100
  if (sortBy === 'volume') {
64
101
  markets.sort((a, b) => b.volume24h - a.volume24h);
@@ -41,6 +41,9 @@ async function main() {
41
41
  console.log('='.repeat(40) + '\n');
42
42
 
43
43
  try {
44
+ // Load metadata (needed for HIP-3 coin resolution)
45
+ await client.getMetaAndAssetCtxs();
46
+
44
47
  let trades = await client.getRecentTrades(coin.toUpperCase());
45
48
 
46
49
  // Most recent first
@@ -228,6 +228,29 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
228
228
  }
229
229
  }
230
230
 
231
+ // Search HIP-3 perps
232
+ if (!typeFilter || typeFilter === 'hip3' || typeFilter === 'perp') {
233
+ try {
234
+ const allPerps = await client.getAllPerpMetas();
235
+ for (const dexData of allPerps) {
236
+ if (!dexData.dexName) continue; // Skip main dex (already searched)
237
+ for (let i = 0; i < dexData.meta.universe.length; i++) {
238
+ const asset = dexData.meta.universe[i];
239
+ if (asset.name.toUpperCase().includes(query)) {
240
+ results.push({
241
+ coin: `${dexData.dexName}:${asset.name}`,
242
+ type: 'hip3',
243
+ dex: dexData.dexName,
244
+ markPx: dexData.assetCtxs[i]?.markPx,
245
+ dayVolume: dexData.assetCtxs[i]?.dayNtlVlm,
246
+ maxLeverage: asset.maxLeverage,
247
+ });
248
+ }
249
+ }
250
+ }
251
+ } catch { /* HIP-3 may not be available */ }
252
+ }
253
+
231
254
  // Search spot
232
255
  if (!typeFilter || typeFilter === 'spot') {
233
256
  try {
@@ -472,6 +495,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
472
495
  '1d': 86_400_000, '3d': 259_200_000, '1w': 604_800_000, '1M': 2_592_000_000,
473
496
  };
474
497
 
498
+ // Load metadata for HIP-3 coin resolution
499
+ await client.getMetaAndAssetCtxs();
475
500
  const now = Date.now();
476
501
  const startTime = now - (bars * (intervalMs[interval] || 3_600_000));
477
502
  const candles = await client.getCandleSnapshot(coin, interval, startTime);
@@ -512,6 +537,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
512
537
  const hours = (params.hours as number) || 24;
513
538
  const startTime = Date.now() - (hours * 3_600_000);
514
539
 
540
+ // Load metadata for HIP-3 coin resolution
541
+ await client.getMetaAndAssetCtxs();
515
542
  const history = await client.getFundingHistory(coin, startTime);
516
543
 
517
544
  const rates = history.map(e => parseFloat(e.fundingRate));
@@ -548,6 +575,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
548
575
  const client = getClient();
549
576
 
550
577
  const coin = (params.coin as string).toUpperCase();
578
+ // Load metadata for HIP-3 coin resolution
579
+ await client.getMetaAndAssetCtxs();
551
580
  let trades = await client.getRecentTrades(coin);
552
581
 
553
582
  trades.sort((a, b) => b.time - a.time);
@@ -600,6 +629,97 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
600
629
  },
601
630
  },
602
631
 
632
+ {
633
+ name: 'ob_funding_scan',
634
+ description: 'Scan funding rates across all dexes (main + HIP-3) for arbitrage opportunities',
635
+ parameters: {
636
+ type: 'object',
637
+ properties: {
638
+ threshold: { type: 'number', description: 'Min annualized funding rate % to show (default: 25)' },
639
+ mainOnly: { type: 'boolean', description: 'Only scan main perps' },
640
+ hip3Only: { type: 'boolean', description: 'Only scan HIP-3 perps' },
641
+ top: { type: 'number', description: 'Number of results (default: 30)' },
642
+ pairs: { type: 'boolean', description: 'Show opposing funding pairs' },
643
+ },
644
+ },
645
+ async execute(_id, params) {
646
+ const { getClient } = await import('../core/client.js');
647
+ const { annualizeFundingRate } = await import('../core/utils.js');
648
+ const client = getClient();
649
+
650
+ const threshold = (params.threshold as number) ?? 25;
651
+ const mainOnly = params.mainOnly as boolean ?? false;
652
+ const hip3Only = params.hip3Only as boolean ?? false;
653
+ const topN = (params.top as number) ?? 30;
654
+
655
+ const allPerps = await client.getAllPerpMetas();
656
+ const results: Array<Record<string, unknown>> = [];
657
+
658
+ for (const dexData of allPerps) {
659
+ const isMain = !dexData.dexName;
660
+ if (mainOnly && !isMain) continue;
661
+ if (hip3Only && isMain) continue;
662
+
663
+ for (let i = 0; i < dexData.meta.universe.length; i++) {
664
+ const asset = dexData.meta.universe[i];
665
+ const ctx = dexData.assetCtxs[i];
666
+ if (!ctx) continue;
667
+
668
+ const hourlyRate = parseFloat(ctx.funding);
669
+ const annualizedPct = annualizeFundingRate(hourlyRate) * 100;
670
+ const openInterest = parseFloat(ctx.openInterest);
671
+
672
+ if (Math.abs(annualizedPct) < threshold) continue;
673
+ if (openInterest < 100) continue;
674
+
675
+ results.push({
676
+ coin: dexData.dexName ? `${dexData.dexName}:${asset.name}` : asset.name,
677
+ dex: dexData.dexName ?? 'main',
678
+ annualizedPct: annualizedPct.toFixed(1),
679
+ direction: hourlyRate > 0 ? 'longs pay shorts' : 'shorts pay longs',
680
+ collectBy: hourlyRate > 0 ? 'SHORT' : 'LONG',
681
+ openInterest: ctx.openInterest,
682
+ markPx: ctx.markPx,
683
+ });
684
+ }
685
+ }
686
+
687
+ results.sort((a, b) => Math.abs(parseFloat(b.annualizedPct as string)) - Math.abs(parseFloat(a.annualizedPct as string)));
688
+
689
+ const output: Record<string, unknown> = {
690
+ threshold,
691
+ scope: mainOnly ? 'main' : hip3Only ? 'hip3' : 'all',
692
+ results: results.slice(0, topN),
693
+ };
694
+
695
+ // Find opposing pairs if requested
696
+ if (params.pairs) {
697
+ const longs = results.filter(r => parseFloat(r.annualizedPct as string) > 0);
698
+ const shorts = results.filter(r => parseFloat(r.annualizedPct as string) < 0);
699
+ const pairs: Array<Record<string, unknown>> = [];
700
+
701
+ for (const l of longs) {
702
+ for (const s of shorts) {
703
+ const spread = parseFloat(l.annualizedPct as string) + Math.abs(parseFloat(s.annualizedPct as string));
704
+ if (spread > 20) {
705
+ pairs.push({
706
+ short: l.coin,
707
+ shortFunding: l.annualizedPct,
708
+ long: s.coin,
709
+ longFunding: s.annualizedPct,
710
+ spreadPct: spread.toFixed(1),
711
+ });
712
+ }
713
+ }
714
+ }
715
+ pairs.sort((a, b) => parseFloat(b.spreadPct as string) - parseFloat(a.spreadPct as string));
716
+ output.opposingPairs = pairs.slice(0, 10);
717
+ }
718
+
719
+ return json(output);
720
+ },
721
+ },
722
+
603
723
  // ── Trading Tools ───────────────────────────────────────────
604
724
 
605
725
  {
@@ -268,11 +268,13 @@ async function main(): Promise<OnboardResult> {
268
268
  console.log('Step 1/3: Wallet Setup');
269
269
  console.log('----------------------');
270
270
  console.log('How would you like to set up your wallet?\n');
271
- console.log(' 1) Import existing private key (master wallet)');
272
- console.log(' 2) Generate a new wallet (master wallet)');
273
- console.log(' 3) Generate API wallet (recommended for agents)');
274
- console.log(' Safer: can trade but cannot withdraw funds.');
275
- console.log(' Requires browser approval from your master wallet.\n');
271
+ console.log(' 1) Generate a fresh wallet (recommended for agents)');
272
+ console.log(' Creates a dedicated trading wallet. Builder fee is auto-approved.');
273
+ console.log(' Just fund it with USDC and start trading — no browser steps needed.');
274
+ console.log('');
275
+ console.log(' 2) Import existing private key');
276
+ console.log(' 3) Generate API wallet (restricted, requires browser approval)');
277
+ console.log(' Can trade but cannot withdraw. Requires master wallet approval in browser.\n');
276
278
 
277
279
  let choice = '';
278
280
  while (choice !== '1' && choice !== '2' && choice !== '3') {
@@ -292,7 +294,7 @@ async function main(): Promise<OnboardResult> {
292
294
  // Options 1 & 2: Master wallet flow
293
295
  let privateKey: `0x${string}`;
294
296
 
295
- if (choice === '1') {
297
+ if (choice === '2') {
296
298
  // User has existing key
297
299
  const rl2 = createReadline();
298
300
  console.log('\nEnter your private key (0x... format):\n');
@@ -313,7 +315,7 @@ async function main(): Promise<OnboardResult> {
313
315
 
314
316
  console.log('\n✅ Private key accepted');
315
317
  } else {
316
- // Generate new wallet
318
+ // Generate new wallet (option 1)
317
319
  console.log('\nGenerating new wallet...');
318
320
  privateKey = generatePrivateKey();
319
321
  console.log('✅ New wallet created');
@@ -385,7 +387,7 @@ HYPERLIQUID_NETWORK=mainnet
385
387
  console.log(`Network: Hyperliquid (Mainnet)`);
386
388
  console.log(`Config: ${CONFIG_PATH}`);
387
389
 
388
- if (choice === '2') {
390
+ if (choice === '1' || choice === '2') {
389
391
  console.log('\n⚠️ IMPORTANT: Save your private key!');
390
392
  console.log('-----------------------------------');
391
393
  console.log(`Private Key: ${privateKey}`);
@@ -95,11 +95,36 @@ async function main() {
95
95
  console.log('===============================\n');
96
96
 
97
97
  try {
98
- // Initial funding check
98
+ // Initial funding check - loads main + HIP-3 assets
99
99
  await client.getMetaAndAssetCtxs();
100
- const meta = await client.getMetaAndAssetCtxs();
101
- const assetIndex = client.getAssetIndex(coin);
102
- const assetCtx = meta.assetCtxs[assetIndex];
100
+
101
+ // For HIP-3 assets, we need to fetch funding from the dex-specific metadata
102
+ let hourlyFunding: number;
103
+ let openInterestVal: number;
104
+
105
+ if (client.isHip3(coin)) {
106
+ const allPerps = await client.getAllPerpMetas();
107
+ const dexName = client.getCoinDex(coin);
108
+ const localName = client.getCoinLocalName(coin);
109
+ const dexData = allPerps.find(d => d.dexName === dexName);
110
+ if (!dexData) {
111
+ console.error(`Error: No market data for HIP-3 dex ${dexName}`);
112
+ process.exit(1);
113
+ }
114
+ const assetIdx = dexData.meta.universe.findIndex(a => a.name === localName);
115
+ if (assetIdx === -1 || !dexData.assetCtxs[assetIdx]) {
116
+ console.error(`Error: No market data for ${coin}`);
117
+ process.exit(1);
118
+ }
119
+ hourlyFunding = parseFloat(dexData.assetCtxs[assetIdx].funding);
120
+ openInterestVal = parseFloat(dexData.assetCtxs[assetIdx].openInterest);
121
+ } else {
122
+ const meta = await client.getMetaAndAssetCtxs();
123
+ const assetIndex = client.getAssetIndex(coin);
124
+ const assetCtx = meta.assetCtxs[assetIndex];
125
+ hourlyFunding = parseFloat(assetCtx.funding);
126
+ openInterestVal = parseFloat(assetCtx.openInterest);
127
+ }
103
128
 
104
129
  const mids = await client.getAllMids();
105
130
  const midPrice = parseFloat(mids[coin]);
@@ -108,9 +133,7 @@ async function main() {
108
133
  process.exit(1);
109
134
  }
110
135
 
111
- const hourlyFunding = parseFloat(assetCtx.funding);
112
136
  const annualizedFunding = annualizeFundingRate(hourlyFunding) * 100; // as percentage
113
- const openInterest = parseFloat(assetCtx.openInterest);
114
137
  const positionSize = notionalSize / midPrice;
115
138
 
116
139
  console.log('Current Market State');
@@ -119,7 +142,7 @@ async function main() {
119
142
  console.log(`Mid Price: ${formatUsd(midPrice)}`);
120
143
  console.log(`Hourly Funding: ${(hourlyFunding * 100).toFixed(4)}%`);
121
144
  console.log(`Annualized: ${annualizedFunding.toFixed(2)}%`);
122
- console.log(`Open Interest: ${formatUsd(openInterest)}`);
145
+ console.log(`Open Interest: ${formatUsd(openInterestVal)}`);
123
146
  console.log(`\nStrategy Config`);
124
147
  console.log('---------------');
125
148
  console.log(`Target Notional: ${formatUsd(notionalSize)}`);
@@ -218,9 +241,19 @@ async function main() {
218
241
  await sleep(checkInterval);
219
242
 
220
243
  // Get updated funding
221
- const newMeta = await client.getMetaAndAssetCtxs();
222
- const newAssetCtx = newMeta.assetCtxs[assetIndex];
223
- const newHourlyFunding = parseFloat(newAssetCtx.funding);
244
+ let newHourlyFunding: number;
245
+ if (client.isHip3(coin)) {
246
+ const freshPerps = await client.getAllPerpMetas();
247
+ const dexName = client.getCoinDex(coin);
248
+ const localName = client.getCoinLocalName(coin);
249
+ const dexData = freshPerps.find(d => d.dexName === dexName);
250
+ const idx = dexData?.meta.universe.findIndex(a => a.name === localName) ?? -1;
251
+ newHourlyFunding = idx >= 0 && dexData?.assetCtxs[idx] ? parseFloat(dexData.assetCtxs[idx].funding) : 0;
252
+ } else {
253
+ const newMeta = await client.getMetaAndAssetCtxs();
254
+ const assetIndex = client.getAssetIndex(coin);
255
+ newHourlyFunding = parseFloat(newMeta.assetCtxs[assetIndex].funding);
256
+ }
224
257
  const newAnnualized = annualizeFundingRate(newHourlyFunding) * 100;
225
258
  const newMids = await client.getAllMids();
226
259
  const newPrice = parseFloat(newMids[coin]);