openbroker 1.0.56 → 1.0.57

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,20 @@
2
2
 
3
3
  All notable changes to Open Broker will be documented in this file.
4
4
 
5
+ ## [1.0.57] - 2026-03-10
6
+
7
+ ### Fixed
8
+ - **Unified Account $0 Equity**: Made account mode detection more robust — handles wrapped API responses, case-insensitive matching. Added debug logging for spot balance lookup to diagnose edge cases. Plugin `ob_account` now returns `accountMode` field.
9
+
10
+ ### Added
11
+ - **`--json` Flag**: Info commands now support `--json` for machine-readable output
12
+ - `account --json` — full account state as JSON
13
+ - `positions --json` — positions array with mark prices and liquidation distances
14
+ - `funding --json` — funding rate data
15
+ - `fills --json` — trade fill history
16
+ - `orders --json` — order history
17
+ - `markets --json` — market data
18
+
5
19
  ## [1.0.56] - 2026-03-10
6
20
 
7
21
  ### 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.56", "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.57", "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.56",
4
+ "version": "1.0.57",
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.56",
3
+ "version": "1.0.57",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -656,14 +656,17 @@ export class HyperliquidClient {
656
656
  }),
657
657
  });
658
658
  const data = await response.json();
659
- this.log('userAbstraction response:', data);
659
+ this.log('userAbstraction response:', JSON.stringify(data));
660
660
 
661
- // API returns: "default" | "disabled" | "dexAbstraction" | "unifiedAccount" | "portfolioMargin"
662
- if (data === 'unifiedAccount') {
661
+ // API may return a bare string or an object. Normalize to string for matching.
662
+ const mode = typeof data === 'string' ? data : (data?.abstraction ?? data?.mode ?? String(data));
663
+ const modeLower = mode.toLowerCase();
664
+
665
+ if (modeLower.includes('unified')) {
663
666
  this.accountMode = 'unified';
664
- } else if (data === 'portfolioMargin') {
667
+ } else if (modeLower.includes('portfolio')) {
665
668
  this.accountMode = 'portfolio';
666
- } else if (data === 'dexAbstraction') {
669
+ } else if (modeLower.includes('dex')) {
667
670
  this.accountMode = 'dexAbstraction';
668
671
  } else {
669
672
  // "default" or "disabled" both mean standard mode
@@ -1173,7 +1176,12 @@ export class HyperliquidClient {
1173
1176
  if (unified) {
1174
1177
  try {
1175
1178
  const spotState = await this.getSpotBalances(user);
1176
- const usdcBalance = spotState.balances.find(b => b.coin === 'USDC');
1179
+ this.log('Unified spot balances:', JSON.stringify(spotState));
1180
+
1181
+ // Find USDC balance (case-insensitive, handles variations)
1182
+ const balances = spotState?.balances ?? [];
1183
+ const usdcBalance = balances.find(b => b.coin?.toUpperCase() === 'USDC');
1184
+
1177
1185
  if (usdcBalance) {
1178
1186
  const totalUsdc = usdcBalance.total;
1179
1187
  const holdUsdc = usdcBalance.hold;
@@ -1200,6 +1208,8 @@ export class HyperliquidClient {
1200
1208
  mainState.crossMarginSummary = { ...summary };
1201
1209
 
1202
1210
  this.log(`Unified account: USDC balance $${parseFloat(totalUsdc).toFixed(2)}, margin used $${totalMarginUsed.toFixed(2)}`);
1211
+ } else {
1212
+ this.log('Unified account: no USDC balance found in spot state. Balances:', balances.map(b => b.coin));
1203
1213
  }
1204
1214
  } catch (err) {
1205
1215
  this.log('Failed to fetch spot balances for unified account:', err instanceof Error ? err.message : String(err));
@@ -6,37 +6,14 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
6
6
 
7
7
  async function main() {
8
8
  const args = parseArgs(process.argv.slice(2));
9
+ const jsonOutput = args.json as boolean;
9
10
  const client = getClient();
10
11
 
11
- console.log('Open Broker - Account Info');
12
- console.log('==========================\n');
13
-
14
- console.log('Wallet Configuration');
15
- console.log('--------------------');
16
- console.log(`Trading Account: ${client.address}`);
17
- console.log(`Signing Wallet: ${client.walletAddress}`);
18
- console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
12
+ if (args.verbose) {
13
+ client.verbose = true;
14
+ }
19
15
 
20
16
  const accountMode = await client.getAccountMode();
21
- const modeLabel: Record<string, string> = {
22
- standard: 'Standard (separate balances per dex)',
23
- unified: 'Unified Account (shared USDC across all dexes)',
24
- portfolio: 'Portfolio Margin',
25
- dexAbstraction: 'DEX Abstraction (deprecated)',
26
- };
27
- console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
28
-
29
- // Check builder fee approval
30
- const builderApproval = await client.getMaxBuilderFee();
31
- console.log(`Builder Address: ${client.builderAddress}`);
32
- console.log(`Builder Fee: ${client.builderFeeBps} bps`);
33
- if (builderApproval) {
34
- console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
35
- } else {
36
- console.log(`Builder Approved: ❌ No`);
37
- console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
38
- }
39
- console.log('');
40
17
 
41
18
  try {
42
19
  const state = await client.getUserStateAll();
@@ -47,6 +24,93 @@ async function main() {
47
24
  const withdrawable = parseFloat(margin.withdrawable);
48
25
  const totalNotional = parseFloat(margin.totalNtlPos);
49
26
 
27
+ const positions = state.assetPositions
28
+ .filter(ap => Math.abs(parseFloat(ap.position.szi)) >= 0.0001)
29
+ .map(ap => {
30
+ const pos = ap.position;
31
+ const size = parseFloat(pos.szi);
32
+ const entryPx = parseFloat(pos.entryPx);
33
+ const notional = parseFloat(pos.positionValue);
34
+ const markPx = Math.abs(notional / size);
35
+ const pnl = parseFloat(pos.unrealizedPnl);
36
+ return {
37
+ coin: pos.coin,
38
+ side: size > 0 ? 'long' : 'short',
39
+ size: pos.szi,
40
+ entryPrice: pos.entryPx,
41
+ markPrice: markPx,
42
+ notional: Math.abs(notional),
43
+ unrealizedPnl: pnl,
44
+ leverage: `${pos.leverage.value}x ${pos.leverage.type}`,
45
+ liquidationPx: pos.liquidationPx,
46
+ };
47
+ });
48
+
49
+ const totalPnl = positions.reduce((sum, p) => sum + p.unrealizedPnl, 0);
50
+
51
+ // JSON output
52
+ if (jsonOutput) {
53
+ const result: Record<string, unknown> = {
54
+ address: client.address,
55
+ signingWallet: client.walletAddress,
56
+ walletType: client.isApiWallet ? 'api' : 'main',
57
+ accountMode,
58
+ equity: accountValue,
59
+ totalNotional,
60
+ totalMarginUsed,
61
+ withdrawable,
62
+ marginRatio: totalMarginUsed > 0 && accountValue > 0 ? totalMarginUsed / accountValue : 0,
63
+ totalUnrealizedPnl: totalPnl,
64
+ positions,
65
+ };
66
+
67
+ if (args.orders) {
68
+ const orders = await client.getOpenOrders();
69
+ result.openOrders = orders.map(o => ({
70
+ coin: o.coin,
71
+ oid: o.oid,
72
+ side: o.side === 'B' ? 'buy' : 'sell',
73
+ size: o.sz,
74
+ price: o.limitPx,
75
+ orderType: o.orderType,
76
+ timestamp: o.timestamp,
77
+ }));
78
+ }
79
+
80
+ console.log(JSON.stringify(result, null, 2));
81
+ return;
82
+ }
83
+
84
+ // Human-readable output
85
+ console.log('Open Broker - Account Info');
86
+ console.log('==========================\n');
87
+
88
+ console.log('Wallet Configuration');
89
+ console.log('--------------------');
90
+ console.log(`Trading Account: ${client.address}`);
91
+ console.log(`Signing Wallet: ${client.walletAddress}`);
92
+ console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
93
+
94
+ const modeLabel: Record<string, string> = {
95
+ standard: 'Standard (separate balances per dex)',
96
+ unified: 'Unified Account (shared USDC across all dexes)',
97
+ portfolio: 'Portfolio Margin',
98
+ dexAbstraction: 'DEX Abstraction (deprecated)',
99
+ };
100
+ console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
101
+
102
+ // Check builder fee approval
103
+ const builderApproval = await client.getMaxBuilderFee();
104
+ console.log(`Builder Address: ${client.builderAddress}`);
105
+ console.log(`Builder Fee: ${client.builderFeeBps} bps`);
106
+ if (builderApproval) {
107
+ console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
108
+ } else {
109
+ console.log(`Builder Approved: ❌ No`);
110
+ console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
111
+ }
112
+ console.log('');
113
+
50
114
  console.log('Margin Summary');
51
115
  console.log('--------------');
52
116
  console.log(`Account Value: ${formatUsd(accountValue)}`);
@@ -62,33 +126,18 @@ async function main() {
62
126
  console.log('\nPositions Summary');
63
127
  console.log('-----------------');
64
128
 
65
- if (state.assetPositions.length === 0) {
129
+ if (positions.length === 0) {
66
130
  console.log('No open positions');
67
131
  } else {
68
- let totalPnl = 0;
69
132
  console.log('Coin | Size | Entry | Mark | PnL | Leverage');
70
133
  console.log('---------|------------|------------|------------|------------|----------');
71
134
 
72
- for (const ap of state.assetPositions) {
73
- const pos = ap.position;
74
- const size = parseFloat(pos.szi);
75
- if (Math.abs(size) < 0.0001) continue;
76
-
77
- const entryPx = parseFloat(pos.entryPx);
78
- const pnl = parseFloat(pos.unrealizedPnl);
79
- totalPnl += pnl;
80
-
81
- // Get mark price from leverage calculation
82
- const notional = parseFloat(pos.positionValue);
83
- const markPx = Math.abs(notional / size);
84
-
85
- const side = size > 0 ? 'L' : 'S';
86
- const leverageStr = `${pos.leverage.value}x ${pos.leverage.type}`;
87
-
135
+ for (const p of positions) {
136
+ const side = p.side === 'long' ? 'L' : 'S';
88
137
  console.log(
89
- `${pos.coin.padEnd(8)} | ${side} ${Math.abs(size).toFixed(4).padStart(8)} | ` +
90
- `${formatUsd(entryPx).padStart(10)} | ${formatUsd(markPx).padStart(10)} | ` +
91
- `${formatUsd(pnl).padStart(10)} | ${leverageStr}`
138
+ `${p.coin.padEnd(8)} | ${side} ${Math.abs(parseFloat(p.size)).toFixed(4).padStart(8)} | ` +
139
+ `${formatUsd(parseFloat(p.entryPrice)).padStart(10)} | ${formatUsd(p.markPrice).padStart(10)} | ` +
140
+ `${formatUsd(p.unrealizedPnl).padStart(10)} | ${p.leverage}`
92
141
  );
93
142
  }
94
143
 
@@ -32,11 +32,9 @@ async function main() {
32
32
  const filterCoin = args.coin as string | undefined;
33
33
  const filterSide = args.side as string | undefined;
34
34
  const top = parseInt(args.top as string) || 20;
35
+ const jsonOutput = args.json as boolean;
35
36
  const client = getClient();
36
37
 
37
- console.log('Open Broker - Trade Fills');
38
- console.log('========================\n');
39
-
40
38
  try {
41
39
  let fills = await client.getUserFills();
42
40
 
@@ -52,6 +50,22 @@ async function main() {
52
50
  fills.sort((a, b) => b.time - a.time);
53
51
  fills = fills.slice(0, top);
54
52
 
53
+ if (jsonOutput) {
54
+ console.log(JSON.stringify(fills.map(f => ({
55
+ time: new Date(f.time).toISOString(),
56
+ coin: f.coin,
57
+ side: f.side === 'B' ? 'buy' : 'sell',
58
+ size: f.sz,
59
+ price: f.px,
60
+ fee: f.fee,
61
+ closedPnl: f.closedPnl,
62
+ })), null, 2));
63
+ return;
64
+ }
65
+
66
+ console.log('Open Broker - Trade Fills');
67
+ console.log('========================\n');
68
+
55
69
  if (fills.length === 0) {
56
70
  console.log('No fills found');
57
71
  return;
@@ -19,9 +19,7 @@ async function main() {
19
19
  const filterCoin = args.coin as string | undefined;
20
20
  const sortBy = (args.sort as string) || 'annualized'; // annualized, hourly, oi
21
21
  const showAll = args.all as boolean;
22
-
23
- console.log('Open Broker - Funding Rates');
24
- console.log('===========================\n');
22
+ const jsonOutput = args.json as boolean;
25
23
 
26
24
  const client = getClient();
27
25
  const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
@@ -101,11 +99,19 @@ async function main() {
101
99
  // Limit
102
100
  const displayData = filterCoin ? fundingData : fundingData.slice(0, topN);
103
101
 
102
+ if (jsonOutput) {
103
+ console.log(JSON.stringify(displayData, null, 2));
104
+ return;
105
+ }
106
+
104
107
  if (displayData.length === 0) {
105
108
  console.log(filterCoin ? `No data for ${filterCoin}` : 'No funding data available');
106
109
  return;
107
110
  }
108
111
 
112
+ console.log('Open Broker - Funding Rates');
113
+ console.log('===========================\n');
114
+
109
115
  // Table header
110
116
  console.log('Coin | Hourly Rate | Annualized | Premium | Open Interest | Mark');
111
117
  console.log('---------|-------------|------------|-------------|---------------|----------');
@@ -21,9 +21,7 @@ async function main() {
21
21
  const filterCoin = args.coin as string | undefined;
22
22
  const topN = parseInt(args.top as string) || 30;
23
23
  const sortBy = (args.sort as string) || 'volume'; // volume, oi, change
24
-
25
- console.log('Open Broker - Markets');
26
- console.log('=====================\n');
24
+ const jsonOutput = args.json as boolean;
27
25
 
28
26
  const client = getClient();
29
27
  const includeHip3 = args['include-hip3'] as boolean || args['hip3'] as boolean || (filterCoin?.includes(':') ?? false);
@@ -109,6 +107,14 @@ async function main() {
109
107
  // Limit
110
108
  const displayData = filterCoin ? markets : markets.slice(0, topN);
111
109
 
110
+ if (jsonOutput) {
111
+ console.log(JSON.stringify(displayData, null, 2));
112
+ return;
113
+ }
114
+
115
+ console.log('Open Broker - Markets');
116
+ console.log('=====================\n');
117
+
112
118
  if (displayData.length === 0) {
113
119
  console.log(filterCoin ? `No data for ${filterCoin}` : 'No market data available');
114
120
  return;
@@ -32,11 +32,9 @@ async function main() {
32
32
  const filterCoin = args.coin as string | undefined;
33
33
  const filterStatus = args.status as string | undefined;
34
34
  const top = parseInt(args.top as string) || 20;
35
+ const jsonOutput = args.json as boolean;
35
36
  const client = getClient();
36
37
 
37
- console.log('Open Broker - Order History');
38
- console.log('==========================\n');
39
-
40
38
  try {
41
39
  let orders = await client.getHistoricalOrders();
42
40
 
@@ -52,6 +50,23 @@ async function main() {
52
50
  orders.sort((a, b) => b.order.timestamp - a.order.timestamp);
53
51
  orders = orders.slice(0, top);
54
52
 
53
+ if (jsonOutput) {
54
+ console.log(JSON.stringify(orders.map(entry => ({
55
+ time: new Date(entry.order.timestamp).toISOString(),
56
+ coin: entry.order.coin,
57
+ side: entry.order.side === 'B' ? 'buy' : 'sell',
58
+ orderType: entry.order.orderType,
59
+ size: entry.order.sz,
60
+ price: entry.order.limitPx,
61
+ status: entry.status,
62
+ oid: entry.order.oid,
63
+ })), null, 2));
64
+ return;
65
+ }
66
+
67
+ console.log('Open Broker - Order History');
68
+ console.log('==========================\n');
69
+
55
70
  if (orders.length === 0) {
56
71
  console.log('No orders found');
57
72
  return;
@@ -7,10 +7,12 @@ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
7
7
  async function main() {
8
8
  const args = parseArgs(process.argv.slice(2));
9
9
  const filterCoin = args.coin as string | undefined;
10
+ const jsonOutput = args.json as boolean;
10
11
  const client = getClient();
11
12
 
12
- console.log('Open Broker - Positions');
13
- console.log('=======================\n');
13
+ if (args.verbose) {
14
+ client.verbose = true;
15
+ }
14
16
 
15
17
  try {
16
18
  const [state, mids, fundingHistory] = await Promise.all([
@@ -26,11 +28,6 @@ async function main() {
26
28
  return true;
27
29
  });
28
30
 
29
- if (positions.length === 0) {
30
- console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
31
- return;
32
- }
33
-
34
31
  // Sum cumulative funding per coin
35
32
  const fundingByCoin = new Map<string, number>();
36
33
  for (const entry of fundingHistory) {
@@ -39,6 +36,44 @@ async function main() {
39
36
  fundingByCoin.set(coin, (fundingByCoin.get(coin) ?? 0) + usdc);
40
37
  }
41
38
 
39
+ // JSON output
40
+ if (jsonOutput) {
41
+ const result = positions.map(ap => {
42
+ const pos = ap.position;
43
+ const size = parseFloat(pos.szi);
44
+ const markPx = parseFloat(mids[pos.coin] || '0');
45
+ const liqPx = pos.liquidationPx ? parseFloat(pos.liquidationPx) : null;
46
+ return {
47
+ coin: pos.coin,
48
+ side: size > 0 ? 'long' : 'short',
49
+ size: pos.szi,
50
+ entryPrice: pos.entryPx,
51
+ markPrice: markPx,
52
+ notional: Math.abs(parseFloat(pos.positionValue)),
53
+ unrealizedPnl: parseFloat(pos.unrealizedPnl),
54
+ returnOnEquity: parseFloat(pos.returnOnEquity),
55
+ cumulativeFunding: fundingByCoin.get(pos.coin) ?? 0,
56
+ marginUsed: parseFloat(pos.marginUsed),
57
+ leverage: `${pos.leverage.value}x`,
58
+ leverageType: pos.leverage.type,
59
+ liquidationPrice: liqPx,
60
+ liquidationDistance: liqPx && markPx ? Math.abs((markPx - liqPx) / markPx) : null,
61
+ maxLeverage: pos.maxLeverage,
62
+ };
63
+ });
64
+ console.log(JSON.stringify(result, null, 2));
65
+ return;
66
+ }
67
+
68
+ // Human-readable output
69
+ console.log('Open Broker - Positions');
70
+ console.log('=======================\n');
71
+
72
+ if (positions.length === 0) {
73
+ console.log(filterCoin ? `No position in ${filterCoin}` : 'No open positions');
74
+ return;
75
+ }
76
+
42
77
  for (const ap of positions) {
43
78
  const pos = ap.position;
44
79
  const size = parseFloat(pos.szi);
@@ -35,10 +35,12 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
35
35
  const { getClient } = await import('../core/client.js');
36
36
  const client = getClient();
37
37
  const state = await client.getUserStateAll();
38
+ const accountMode = await client.getAccountMode();
38
39
 
39
40
  const result: Record<string, unknown> = {
40
41
  address: client.address,
41
42
  isApiWallet: client.isApiWallet,
43
+ accountMode,
42
44
  equity: state.marginSummary.accountValue,
43
45
  totalNtlPos: state.marginSummary.totalNtlPos,
44
46
  totalMarginUsed: state.marginSummary.totalMarginUsed,