openbroker 1.0.68 → 1.0.70

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.68", "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.70", "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 ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -732,15 +732,86 @@ api.on('margin_warning', async ({ marginUsedPct, equity }) => {
732
732
 
733
733
  ### Client Methods Available
734
734
 
735
- The `api.client` object exposes the full Hyperliquid SDK:
735
+ The `api.client` object exposes the full `HyperliquidClient`. All `coin` params accept HIP-3 prefixed tickers (e.g. `xyz:CL`). Optional `user` params default to the configured wallet address.
736
+
737
+ #### Trading
738
+
739
+ | Method | Description |
740
+ |--------|-------------|
741
+ | `marketOrder(coin, isBuy, size, slippageBps?, leverage?)` | Market order via IOC limit at mid ± slippage. Returns `OrderResponse` |
742
+ | `limitOrder(coin, isBuy, size, price, tif?, reduceOnly?, leverage?)` | Limit order. `tif`: `'Gtc'` (default), `'Ioc'`, `'Alo'`. Returns `OrderResponse` |
743
+ | `triggerOrder(coin, isBuy, size, triggerPrice, limitPrice, tpsl, reduceOnly?, leverage?)` | Trigger (conditional) order. `tpsl`: `'tp'` or `'sl'`. Activates when price hits `triggerPrice`, then fills as limit at `limitPrice`. Returns `OrderResponse` |
744
+ | `stopLoss(coin, isBuy, size, triggerPrice, slippageBps?)` | Stop loss shortcut. Sets limit price with slippage buffer (default 100 bps / 1%) to ensure fill. `reduceOnly` is always true. Returns `OrderResponse` |
745
+ | `takeProfit(coin, isBuy, size, triggerPrice)` | Take profit shortcut. Limit price = trigger price (favorable direction). `reduceOnly` is always true. Returns `OrderResponse` |
746
+ | `cancel(coin, oid)` | Cancel a single order by numeric OID. Returns `CancelResponse` |
747
+ | `cancelAll(coin?)` | Cancel all open orders. If `coin` is provided, only cancels orders for that asset. Returns `CancelResponse[]` |
748
+ | `order(coin, isBuy, size, price, orderType, reduceOnly?, includeBuilder?, leverage?)` | Low-level order placement. `orderType`: `{ limit: { tif: 'Gtc' | 'Ioc' | 'Alo' } }`. Automatically injects builder fee, rounds price/size, and handles HIP-3 margin setup. Returns `OrderResponse` |
749
+
750
+ #### Market Data
751
+
752
+ | Method | Returns |
753
+ |--------|---------|
754
+ | `getAllMids()` | `Record<string, string>` — mid prices for all assets (main + HIP-3). Key = coin name, value = price string |
755
+ | `getMetaAndAssetCtxs()` | `MetaAndAssetCtxs` — market metadata (universe of assets with `szDecimals`, `maxLeverage`) and asset contexts (funding, open interest, volume, mark/oracle prices) |
756
+ | `getL2Book(coin)` | `{ bids, asks, bestBid, bestAsk, midPrice, spread, spreadBps }` — L2 order book with computed spread |
757
+ | `getRecentTrades(coin)` | `Array<{ coin, side, px, sz, time, hash, tid }>` — recent trade tape. `side`: `'B'` (buy) or `'A'` (sell) |
758
+ | `getCandleSnapshot(coin, interval, startTime, endTime?)` | `Array<{ t, T, s, i, o, c, h, l, v, n }>` — OHLCV candles. `interval`: `'1m'`, `'5m'`, `'15m'`, `'1h'`, `'4h'`, `'1d'`. Times are Unix ms |
759
+ | `getFundingHistory(coin, startTime, endTime?)` | `Array<{ coin, fundingRate, premium, time }>` — historical hourly funding rates |
760
+ | `getPredictedFundings()` | `Array<[coin, Array<[venue, { fundingRate, nextFundingTime }]>]>` — predicted funding rates across all venues |
761
+ | `getPerpDexs()` | `Array<{ name, fullName, deployer } | null>` — list of perp DEXs. Index 0 is `null` (main), rest are HIP-3 |
762
+ | `getAllPerpMetas()` | `Array<{ dexName, meta, assetCtxs }>` — metadata + contexts for every perp DEX (main + all HIP-3) |
763
+ | `getSpotMeta()` | `{ tokens, universe }` — spot market metadata (token info, trading pairs) |
764
+ | `getSpotMetaAndAssetCtxs()` | `{ meta, assetCtxs }` — spot metadata + price/volume contexts |
765
+ | `getTokenDetails(tokenId)` | Token details: supply, deployer, prices. Returns `null` if not found |
766
+
767
+ #### Account
768
+
769
+ | Method | Returns |
770
+ |--------|---------|
771
+ | `getUserStateAll(user?)` | `ClearinghouseState` — full account state across all dexes: `marginSummary` (accountValue, totalMarginUsed, withdrawable), `crossMarginSummary`, and `assetPositions[]` (each with `position.coin`, `.szi`, `.entryPx`, `.unrealizedPnl`, `.positionValue`, `.leverage`, `.marginUsed`, `.liquidationPx`) |
772
+ | `getUserState(user?, dex?)` | `ClearinghouseState` — account state for a single dex (omit `dex` for main perps) |
773
+ | `getOpenOrders(user?)` | `OpenOrder[]` — all open orders across all dexes. Each: `{ coin, side, limitPx, sz, oid, timestamp, orderType }` |
774
+ | `getUserFills(user?, aggregateByTime?)` | `Array<{ coin, px, sz, side, time, closedPnl, fee, oid, tid, crossed, builderFee }>` — trade fill history. `side`: `'B'` (buy) or `'A'` (sell) |
775
+ | `getHistoricalOrders(user?)` | `Array<{ order: { coin, side, limitPx, sz, origSz, oid, timestamp, orderType, tif, triggerCondition, triggerPx, isTrigger, isPositionTpsl, reduceOnly }, status, statusTimestamp }>` — all orders (filled, cancelled, etc.) |
776
+ | `getOrderStatus(oid, user?)` | `{ status, order? }` — status of a specific order by numeric OID or string CLOID |
777
+ | `getUserFunding(user?, startTime?, endTime?)` | `Array<{ time, hash, delta: { coin, usdc, szi, fundingRate } }>` — funding payments received/paid |
778
+ | `getUserFees(user?)` | `{ dailyUserVlm, feeSchedule, userCrossRate, userAddRate, activeReferralDiscount, activeStakingDiscount }` — fee tier, rates, and volume |
779
+ | `getUserRateLimit(user?)` | `{ cumVlm, nRequestsUsed, nRequestsCap, nRequestsSurplus }` — API rate limit status |
780
+ | `getSpotBalances(user?)` | `{ balances: Array<{ coin, token, hold, total, entryNtl }> }` — spot token balances |
781
+ | `getSubAccounts(user?)` | `Array<{ subAccountUser, name }>` — sub-accounts for a master wallet |
782
+ | `getAccountMode(user?)` | `string` — account abstraction mode: `'standard'`, `'unified'`, `'portfolio'`, or `'dexAbstraction'` |
783
+ | `isUnifiedAccount(user?)` | `boolean` — `true` if unified or portfolio margin (shared USDC across dexes) |
784
+
785
+ #### Leverage & Config
786
+
787
+ | Method | Description |
788
+ |--------|-------------|
789
+ | `updateLeverage(coin, leverage, isCross?)` | Set leverage. `isCross` defaults to `true` (cross margin). HIP-3 assets are forced to isolated and clamped to their max leverage |
790
+ | `approveBuilderFee(maxFeeRate?, builder?)` | Approve builder fee (must be called from main wallet, not API wallet). Default rate: `'0.1%'` |
791
+ | `getMaxBuilderFee(user?, builder?)` | Check approved builder fee. Returns fee string (e.g. `'0.01%'`) or `null` if not approved |
792
+
793
+ #### Utility Properties
736
794
 
737
- **Trading:** `marketOrder(coin, isBuy, size)`, `limitOrder(coin, isBuy, size, price)`, `triggerOrder(coin, isBuy, size, triggerPx, isMarket)`, `takeProfit(coin, isBuy, size, triggerPx)`, `stopLoss(coin, isBuy, size, triggerPx)`, `cancel(coin, oid)`, `cancelAll(coin?)`
738
-
739
- **Market Data:** `getAllMids()`, `getMetaAndAssetCtxs()`, `getRecentTrades(coin)`, `getCandleSnapshot(coin, interval)`, `getFundingHistory(coin)`, `getPredictedFundings()`
795
+ | Property / Method | Description |
796
+ |-------------------|-------------|
797
+ | `getAssetIndex(coin)` | Get numeric asset index for a coin (used internally for order wire) |
798
+ | `getSzDecimals(coin)` | Get size decimal precision for a coin |
799
+ | `isHip3(coin)` | Check if a coin is a HIP-3 asset |
800
+ | `getCoinDex(coin)` | Get dex name for a coin (`null` for main perps) |
801
+ | `getAllAssetNames()` | Get all known asset names (main + HIP-3) |
802
+ | `getHip3AssetNames()` | Get only HIP-3 asset names |
803
+ | `invalidateMetaCache()` | Force refresh of market metadata on next call |
740
804
 
741
- **Account:** `getUserStateAll()`, `getOpenOrders()`, `getUserFills()`, `getUserFunding()`, `getHistoricalOrders()`, `getUserFees()`, `getUserRateLimit()`, `getSpotBalances()`
805
+ #### Utility Functions (`api.utils`)
742
806
 
743
- **Leverage:** `updateLeverage(coin, leverage, isIsolated?)`
807
+ | Function | Description |
808
+ |----------|-------------|
809
+ | `roundPrice(price, szDecimals, isSpot?)` | Round price to 5 significant figures (max 6 decimals perp, 8 spot) |
810
+ | `roundSize(size, szDecimals)` | Round size to asset-specific decimal precision |
811
+ | `sleep(ms)` | Promise-based delay |
812
+ | `normalizeCoin(coin)` | Normalize coin name (uppercase, trim whitespace) |
813
+ | `formatUsd(amount)` | Format number as USD string (e.g. `$1,234.56`) |
814
+ | `annualizeFundingRate(hourlyRate)` | Convert hourly funding rate to annualized percentage |
744
815
 
745
816
  ### Example: Price Breakout
746
817
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.68",
4
+ "version": "1.0.70",
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.68",
3
+ "version": "1.0.70",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1126,7 +1126,18 @@ export class HyperliquidClient {
1126
1126
  const params: { user: string; dex?: string } = { user: user ?? this.address };
1127
1127
  if (dex !== undefined) params.dex = dex;
1128
1128
  const response = await this.info.clearinghouseState(params as any);
1129
- return response as ClearinghouseState;
1129
+
1130
+ // The SDK response has `withdrawable` as a top-level field, not inside
1131
+ // marginSummary/crossMarginSummary. Copy it into our MarginSummary shape.
1132
+ const state = response as unknown as ClearinghouseState;
1133
+ const withdrawable = (response as any).withdrawable ?? '0';
1134
+ if (state.marginSummary) {
1135
+ state.marginSummary.withdrawable = withdrawable;
1136
+ }
1137
+ if (state.crossMarginSummary) {
1138
+ state.crossMarginSummary.withdrawable = withdrawable;
1139
+ }
1140
+ return state;
1130
1141
  }
1131
1142
 
1132
1143
  /**
@@ -1142,6 +1153,7 @@ export class HyperliquidClient {
1142
1153
  const dexs = await this.getPerpDexs();
1143
1154
 
1144
1155
  // Collect positions from all HIP-3 dexes
1156
+ let hip3Errors = 0;
1145
1157
  for (let i = 1; i < dexs.length; i++) {
1146
1158
  const dex = dexs[i];
1147
1159
  if (!dex) continue;
@@ -1156,22 +1168,32 @@ export class HyperliquidClient {
1156
1168
  if (!unified) {
1157
1169
  const dexMargin = dexState.marginSummary;
1158
1170
  if (dexMargin) {
1171
+ const safeAdd = (a: string | undefined, b: string | undefined): string => {
1172
+ const va = parseFloat(a ?? '0') || 0;
1173
+ const vb = parseFloat(b ?? '0') || 0;
1174
+ return String(va + vb);
1175
+ };
1159
1176
  const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
1160
- summary.accountValue = String(parseFloat(summary.accountValue) + parseFloat(dexMargin.accountValue));
1161
- summary.totalNtlPos = String(parseFloat(summary.totalNtlPos) + parseFloat(dexMargin.totalNtlPos));
1162
- summary.totalRawUsd = String(parseFloat(summary.totalRawUsd) + parseFloat(dexMargin.totalRawUsd));
1163
- summary.totalMarginUsed = String(parseFloat(summary.totalMarginUsed) + parseFloat(dexMargin.totalMarginUsed));
1164
- summary.withdrawable = String(parseFloat(summary.withdrawable) + parseFloat(dexMargin.withdrawable));
1177
+ summary.accountValue = safeAdd(summary.accountValue, dexMargin.accountValue);
1178
+ summary.totalNtlPos = safeAdd(summary.totalNtlPos, dexMargin.totalNtlPos);
1179
+ summary.totalRawUsd = safeAdd(summary.totalRawUsd, dexMargin.totalRawUsd);
1180
+ summary.totalMarginUsed = safeAdd(summary.totalMarginUsed, dexMargin.totalMarginUsed);
1181
+ summary.withdrawable = safeAdd(summary.withdrawable, dexMargin.withdrawable);
1165
1182
  };
1166
1183
  addToSummary(mainState.marginSummary);
1167
1184
  addToSummary(mainState.crossMarginSummary);
1168
1185
  }
1169
1186
  }
1170
1187
  } catch (err) {
1188
+ hip3Errors++;
1171
1189
  this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
1172
1190
  }
1173
1191
  }
1174
1192
 
1193
+ if (hip3Errors > 0) {
1194
+ this.log(`Warning: ${hip3Errors} HIP-3 dex queries failed — some positions may be missing. Use --verbose for details.`);
1195
+ }
1196
+
1175
1197
  // For unified accounts: equity is the USDC balance from spot clearinghouse
1176
1198
  if (unified) {
1177
1199
  try {
@@ -109,6 +109,13 @@ async function main() {
109
109
  console.log(`Builder Approved: ❌ No`);
110
110
  console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
111
111
  }
112
+
113
+ // Warn if API wallet setup looks misconfigured
114
+ if (!client.isApiWallet && accountValue === 0 && positions.length === 0) {
115
+ console.log('\n⚠️ No positions and $0 equity.');
116
+ console.log(' If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
117
+ console.log(' in ~/.openbroker/.env to the master account address (the wallet that holds funds).');
118
+ }
112
119
  console.log('');
113
120
 
114
121
  console.log('Margin Summary');
@@ -71,6 +71,10 @@ async function main() {
71
71
 
72
72
  if (positions.length === 0) {
73
73
  console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
74
+ if (!filterCoin && !client.isApiWallet) {
75
+ console.log('\n⚠️ If this account is traded via an API wallet, set HYPERLIQUID_ACCOUNT_ADDRESS');
76
+ console.log(' in ~/.openbroker/.env to the master account address.');
77
+ }
74
78
  return;
75
79
  }
76
80
 
@@ -89,6 +89,11 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
89
89
  }));
90
90
  }
91
91
 
92
+ // Warn if likely misconfigured API wallet (querying the API wallet address instead of master)
93
+ if (!client.isApiWallet && accountValue === 0 && state.assetPositions.filter(ap => parseFloat(ap.position.szi) !== 0).length === 0) {
94
+ result.warning = 'No positions and $0 equity. If using an API wallet, ensure HYPERLIQUID_ACCOUNT_ADDRESS is set to the master account address in ~/.openbroker/.env or plugin config.';
95
+ }
96
+
92
97
  return json(result);
93
98
  },
94
99
  },
@@ -231,34 +231,85 @@ async function main(): Promise<OnboardResult> {
231
231
 
232
232
  // Check if config already exists
233
233
  if (fs.existsSync(CONFIG_PATH)) {
234
- console.log('⚠️ Config already exists!');
235
- console.log(` Location: ${CONFIG_PATH}\n`);
236
-
237
- // Read existing config and show wallet address
238
234
  const envContent = fs.readFileSync(CONFIG_PATH, 'utf-8');
239
235
  const keyMatch = envContent.match(/HYPERLIQUID_PRIVATE_KEY=0x([a-fA-F0-9]{64})/);
240
236
 
241
- if (keyMatch) {
242
- const existingKey = `0x${keyMatch[1]}` as `0x${string}`;
243
- const account = privateKeyToAccount(existingKey);
244
- console.log('Current Configuration');
245
- console.log('---------------------');
246
- console.log(`Wallet Address: ${account.address}`);
247
- console.log(`Config File: ${CONFIG_PATH}`);
248
- console.log(`\nTo reconfigure, delete the config file first:`);
249
- console.log(` rm ${CONFIG_PATH}`);
250
- console.log(`\nTo fund this wallet, send USDC on Arbitrum, then deposit at:`);
251
- console.log(` https://app.hyperliquid.xyz/`);
252
-
237
+ if (!keyMatch) {
253
238
  return {
254
- success: true,
255
- walletAddress: account.address,
239
+ success: false,
240
+ error: 'Invalid config file - missing or malformed private key',
256
241
  };
257
242
  }
258
243
 
244
+ const existingKey = `0x${keyMatch[1]}` as `0x${string}`;
245
+ const account = privateKeyToAccount(existingKey);
246
+
247
+ // Check if this is an incomplete API wallet setup (HYPERLIQUID_ACCOUNT_ADDRESS missing or commented out)
248
+ const hasAccountAddress = /^HYPERLIQUID_ACCOUNT_ADDRESS=0x[a-fA-F0-9]{40}/m.test(envContent);
249
+ const isIncompleteApiWallet = envContent.includes('INCOMPLETE') || envContent.includes('# HYPERLIQUID_ACCOUNT_ADDRESS');
250
+
251
+ if (!hasAccountAddress && isIncompleteApiWallet) {
252
+ console.log('⚠️ Incomplete API wallet setup detected!');
253
+ console.log(` API Wallet: ${account.address}`);
254
+ console.log(` Master account address is missing — re-polling for approval...\n`);
255
+
256
+ const approveUrl = `${OPENBROKER_URL}/approve?agent=${account.address}`;
257
+ console.log(` If not yet approved, visit: ${approveUrl}\n`);
258
+
259
+ const masterAddress = await pollForApproval(account.address);
260
+
261
+ if (masterAddress) {
262
+ console.log(`\n✅ Master wallet detected: ${masterAddress}`);
263
+
264
+ // Verify builder fee on-chain
265
+ console.log(' Verifying builder fee approval...');
266
+ const feeApproved = await verifyBuilderFee(masterAddress);
267
+ if (feeApproved) {
268
+ console.log(' ✅ Builder fee: approved on-chain');
269
+ } else {
270
+ console.log(' ⚠️ Builder fee not yet confirmed on-chain (may take a moment)');
271
+ }
272
+
273
+ // Save complete config
274
+ const completeEnv = buildApiWalletEnvContent(existingKey, masterAddress);
275
+ fs.writeFileSync(CONFIG_PATH, completeEnv, { mode: 0o600 });
276
+
277
+ console.log(`\n✅ Config updated: ${CONFIG_PATH}`);
278
+ console.log(` API Wallet: ${account.address}`);
279
+ console.log(` Master Account: ${masterAddress}`);
280
+ console.log('\n Start trading: openbroker account');
281
+
282
+ return { success: true, walletAddress: account.address };
283
+ }
284
+
285
+ console.log('\n⚠️ Approval still not completed.');
286
+ console.log(` Visit: ${approveUrl}`);
287
+ console.log(' Then re-run: openbroker setup\n');
288
+ return { success: false, error: 'Approval not completed' };
289
+ }
290
+
291
+ // Config exists and is complete
292
+ console.log('⚠️ Config already exists!');
293
+ console.log(` Location: ${CONFIG_PATH}\n`);
294
+ console.log('Current Configuration');
295
+ console.log('---------------------');
296
+ console.log(`Wallet Address: ${account.address}`);
297
+ if (hasAccountAddress) {
298
+ const addrMatch = envContent.match(/HYPERLIQUID_ACCOUNT_ADDRESS=(0x[a-fA-F0-9]+)/);
299
+ if (addrMatch) {
300
+ console.log(`Master Account: ${addrMatch[1]}`);
301
+ console.log(`Wallet Type: API Wallet`);
302
+ }
303
+ }
304
+ console.log(`Config File: ${CONFIG_PATH}`);
305
+ console.log(`\nTo reconfigure, delete the config file first:`);
306
+ console.log(` rm ${CONFIG_PATH}`);
307
+ console.log(`\nTo fund this wallet, send USDC on Arbitrum, then deposit at:`);
308
+ console.log(` https://app.hyperliquid.xyz/`);
309
+
259
310
  return {
260
- success: false,
261
- error: 'Invalid config file - missing or malformed private key',
311
+ success: true,
312
+ walletAddress: account.address,
262
313
  };
263
314
  }
264
315