openbroker 1.0.82 → 1.0.87

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.
@@ -32,6 +32,7 @@ Examples:
32
32
 
33
33
  interface FundingScanResult {
34
34
  coin: string;
35
+ assetId: number;
35
36
  dex: string;
36
37
  hourlyRate: number;
37
38
  annualizedPct: number;
@@ -69,9 +70,14 @@ async function scanFunding(client: ReturnType<typeof getClient>, options: {
69
70
 
70
71
  // API returns HIP-3 names already prefixed (e.g., "xyz:CL")
71
72
  const coin = asset.name;
73
+ let assetId = isMain ? i : -1;
74
+ if (!isMain) {
75
+ try { assetId = client.getAssetIndex(coin); } catch { /* not registered */ }
76
+ }
72
77
 
73
78
  results.push({
74
79
  coin,
80
+ assetId,
75
81
  dex: dexData.dexName ?? 'main',
76
82
  hourlyRate,
77
83
  annualizedPct,
@@ -6,6 +6,7 @@ import { formatPercent, annualizeFundingRate, parseArgs } from '../core/utils.js
6
6
 
7
7
  interface FundingDisplay {
8
8
  coin: string;
9
+ assetId: number;
9
10
  hourlyRate: number;
10
11
  annualizedRate: number;
11
12
  premium: number;
@@ -38,7 +39,7 @@ async function main() {
38
39
 
39
40
  const hourlyRate = parseFloat(ctx.funding);
40
41
  const annualizedRate = annualizeFundingRate(hourlyRate);
41
- const premium = parseFloat(ctx.premium);
42
+ const premium = parseFloat(ctx.premium ?? '0');
42
43
  const openInterest = parseFloat(ctx.openInterest);
43
44
  const markPx = parseFloat(ctx.markPx);
44
45
 
@@ -47,6 +48,7 @@ async function main() {
47
48
 
48
49
  fundingData.push({
49
50
  coin: asset.name,
51
+ assetId: i,
50
52
  hourlyRate,
51
53
  annualizedRate,
52
54
  premium,
@@ -84,8 +86,12 @@ async function main() {
84
86
 
85
87
  if (!showAll && openInterest < 1000) continue;
86
88
 
89
+ let assetId = -1;
90
+ try { assetId = client.getAssetIndex(coinName); } catch { /* not registered */ }
91
+
87
92
  fundingData.push({
88
93
  coin: coinName,
94
+ assetId,
89
95
  hourlyRate,
90
96
  annualizedRate,
91
97
  premium: 0,
@@ -6,6 +6,7 @@ import { formatUsd, parseArgs } from '../core/utils.js';
6
6
 
7
7
  interface MarketDisplay {
8
8
  coin: string;
9
+ assetId: number;
9
10
  markPx: number;
10
11
  oraclePx: number;
11
12
  prevDayPx: number;
@@ -47,6 +48,7 @@ async function main() {
47
48
 
48
49
  markets.push({
49
50
  coin: asset.name,
51
+ assetId: i,
50
52
  markPx,
51
53
  oraclePx,
52
54
  prevDayPx,
@@ -86,9 +88,12 @@ async function main() {
86
88
  const volume24h = parseFloat(ctx.dayNtlVlm);
87
89
  const openInterest = parseFloat(ctx.openInterest);
88
90
  const change24h = prevDayPx > 0 ? (markPx - prevDayPx) / prevDayPx : 0;
91
+ let assetId = -1;
92
+ try { assetId = client.getAssetIndex(coinName); } catch { /* not registered */ }
89
93
 
90
94
  markets.push({
91
95
  coin: coinName,
96
+ assetId,
92
97
  markPx,
93
98
  oraclePx,
94
99
  prevDayPx,
@@ -11,6 +11,7 @@ Usage: openbroker order-status --oid <order-id>
11
11
  Options:
12
12
  --oid <id> Order ID (number) or client order ID (hex string) — required
13
13
  --address <0x...> Look up order on another account
14
+ --json Output as JSON (machine-readable)
14
15
  --help, -h Show this help
15
16
 
16
17
  Examples:
@@ -37,15 +38,23 @@ async function main() {
37
38
 
38
39
  const oid = oidArg.startsWith('0x') ? oidArg : parseInt(oidArg);
39
40
  const targetAddress = args.address as string | undefined;
41
+ const jsonOutput = args.json as boolean;
40
42
  const client = getClient();
41
43
  const lookupAddress = targetAddress?.toLowerCase();
42
44
 
43
- console.log('Open Broker - Order Status');
44
- console.log('=========================\n');
45
+ if (!jsonOutput) {
46
+ console.log('Open Broker - Order Status');
47
+ console.log('=========================\n');
48
+ }
45
49
 
46
50
  try {
47
51
  const result = await client.getOrderStatus(oid, lookupAddress);
48
52
 
53
+ if (jsonOutput) {
54
+ console.log(JSON.stringify(result, null, 2));
55
+ return;
56
+ }
57
+
49
58
  if (result.status === 'unknownOid') {
50
59
  console.log(`Order ${oidArg} not found`);
51
60
  return;
@@ -9,6 +9,7 @@ function printUsage() {
9
9
  Usage: openbroker rate-limit [options]
10
10
 
11
11
  Options:
12
+ --json Output as JSON (machine-readable)
12
13
  --help, -h Show this help
13
14
 
14
15
  Examples:
@@ -24,14 +25,22 @@ async function main() {
24
25
  process.exit(0);
25
26
  }
26
27
 
28
+ const jsonOutput = args.json as boolean;
27
29
  const client = getClient();
28
30
 
29
- console.log('Open Broker - API Rate Limit');
30
- console.log('===========================\n');
31
+ if (!jsonOutput) {
32
+ console.log('Open Broker - API Rate Limit');
33
+ console.log('===========================\n');
34
+ }
31
35
 
32
36
  try {
33
37
  const rl = await client.getUserRateLimit();
34
38
 
39
+ if (jsonOutput) {
40
+ console.log(JSON.stringify(rl, null, 2));
41
+ return;
42
+ }
43
+
35
44
  const used = rl.nRequestsUsed;
36
45
  const cap = rl.nRequestsCap;
37
46
  const surplus = rl.nRequestsSurplus;
@@ -7,6 +7,7 @@ interface Args {
7
7
  query: string;
8
8
  type?: 'perp' | 'spot' | 'hip3' | 'all';
9
9
  verbose?: boolean;
10
+ json?: boolean;
10
11
  }
11
12
 
12
13
  function parseArgs(): Args {
@@ -23,6 +24,8 @@ function parseArgs(): Args {
23
24
  }
24
25
  } else if (arg === '--verbose') {
25
26
  args.verbose = true;
27
+ } else if (arg === '--json') {
28
+ args.json = true;
26
29
  } else if (arg === '--help' || arg === '-h') {
27
30
  console.log(`
28
31
  Search Markets - Find assets across all Hyperliquid markets
@@ -32,6 +35,7 @@ Usage: npx tsx scripts/info/search-markets.ts --query <search> [options]
32
35
  Options:
33
36
  --query <search> Search term (required) - matches coin name
34
37
  --type <type> Filter by market type: perp, spot, hip3, or all (default: all)
38
+ --json Output as JSON (machine-readable)
35
39
  --verbose Show detailed output
36
40
  --help Show this help
37
41
 
@@ -40,6 +44,7 @@ Examples:
40
44
  npx tsx scripts/info/search-markets.ts --query BTC # Find all BTC markets
41
45
  npx tsx scripts/info/search-markets.ts --query ETH --type perp # ETH perps only
42
46
  npx tsx scripts/info/search-markets.ts --query PURR --type spot # PURR spot only
47
+ npx tsx scripts/info/search-markets.ts --query HYPE --json # JSON output
43
48
  `);
44
49
  process.exit(0);
45
50
  } else if (!args.query && !arg.startsWith('-')) {
@@ -85,18 +90,23 @@ async function main() {
85
90
  client.verbose = args.verbose ?? false;
86
91
 
87
92
  const query = args.query.toUpperCase();
88
- console.log(`Searching for "${query}" across all markets...\n`);
93
+ if (!args.json) {
94
+ console.log(`Searching for "${query}" across all markets...\n`);
95
+ }
89
96
 
90
- const results: Array<{
97
+ interface Result {
91
98
  type: 'perp' | 'spot' | 'hip3';
92
99
  provider: string;
93
100
  coin: string;
101
+ assetId: number;
94
102
  price: string;
95
103
  volume24h: number;
96
104
  funding?: string;
97
105
  maxLeverage?: number;
98
106
  openInterest?: string;
99
- }> = [];
107
+ }
108
+
109
+ const results: Result[] = [];
100
110
 
101
111
  // Search main perps
102
112
  if (args.type === 'all' || args.type === 'perp') {
@@ -110,6 +120,7 @@ async function main() {
110
120
  type: 'perp',
111
121
  provider: 'Hyperliquid',
112
122
  coin: asset.name,
123
+ assetId: i,
113
124
  price: ctx.markPx,
114
125
  volume24h: parseFloat(ctx.dayNtlVlm),
115
126
  funding: ctx.funding,
@@ -126,7 +137,7 @@ async function main() {
126
137
  // On testnet, load specific dex if query is "dex:COIN" format
127
138
  if (args.query.includes(':')) {
128
139
  await client.loadSingleHip3Dex(args.query.split(':')[0]);
129
- } else {
140
+ } else if (!args.json) {
130
141
  console.log(' (Testnet: HIP-3 dexes not auto-loaded. Use "dexName:COIN" to search a specific dex.)\n');
131
142
  }
132
143
  }
@@ -143,10 +154,13 @@ async function main() {
143
154
  if (!asset || !ctx) continue;
144
155
 
145
156
  if (asset.name.toUpperCase().includes(query)) {
157
+ let assetId = -1;
158
+ try { assetId = client.getAssetIndex(asset.name); } catch { /* not registered */ }
146
159
  results.push({
147
160
  type: 'hip3',
148
161
  provider: dexData.dexName || `HIP-3 DEX ${dexIdx}`,
149
162
  coin: asset.name,
163
+ assetId,
150
164
  price: ctx.markPx,
151
165
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
152
166
  funding: ctx.funding,
@@ -199,6 +213,7 @@ async function main() {
199
213
  type: 'spot',
200
214
  provider: 'Spot',
201
215
  coin: displayName,
216
+ assetId: 10000 + pair.index,
202
217
  price: ctx.markPx,
203
218
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
204
219
  });
@@ -212,20 +227,25 @@ async function main() {
212
227
  // Sort by volume
213
228
  results.sort((a, b) => b.volume24h - a.volume24h);
214
229
 
230
+ if (args.json) {
231
+ console.log(JSON.stringify(results, null, 2));
232
+ return;
233
+ }
234
+
215
235
  if (results.length === 0) {
216
236
  console.log(`No markets found matching "${query}"`);
217
237
  return;
218
238
  }
219
239
 
220
240
  console.log(`Found ${results.length} market(s) matching "${query}":\n`);
221
- console.log('Type Provider Coin Price 24h Volume Funding (Ann.) OI');
222
- console.log('-'.repeat(100));
241
+ console.log('Type Provider Coin AssetID Price 24h Volume Funding (Ann.) OI');
242
+ console.log('-'.repeat(112));
223
243
 
224
244
  for (const m of results) {
225
245
  const typeStr = m.type === 'hip3' ? 'HIP-3' : m.type.charAt(0).toUpperCase() + m.type.slice(1);
226
246
  const oi = m.openInterest ? formatVolume(parseFloat(m.openInterest)) : '-';
227
247
  console.log(
228
- `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
248
+ `${typeStr.padEnd(8)} ${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${(m.funding ? formatFunding(m.funding) : '-').padStart(14)} ${oi.padStart(10)}`
229
249
  );
230
250
  }
231
251
 
@@ -245,11 +265,11 @@ async function main() {
245
265
  const perpsWithFunding = markets.filter((m) => m.funding && m.type !== 'spot');
246
266
  if (perpsWithFunding.length > 1) {
247
267
  console.log(`\n=== ${coin} Funding Comparison ===\n`);
248
- console.log('Provider Coin Funding (Ann.) Price');
249
- console.log('-'.repeat(65));
268
+ console.log('Provider Coin AssetID Funding (Ann.) Price');
269
+ console.log('-'.repeat(78));
250
270
  for (const m of perpsWithFunding.sort((a, b) => parseFloat(b.funding!) - parseFloat(a.funding!))) {
251
271
  console.log(
252
- `${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${formatFunding(m.funding!).padStart(14)} ${formatPrice(m.price)}`
272
+ `${m.provider.padEnd(16)} ${m.coin.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatFunding(m.funding!).padStart(14)} ${formatPrice(m.price)}`
253
273
  );
254
274
  }
255
275
  }
@@ -9,6 +9,7 @@ interface Args {
9
9
  top?: number;
10
10
  verbose?: boolean;
11
11
  address?: string;
12
+ json?: boolean;
12
13
  }
13
14
 
14
15
  function parseArgs(): Args {
@@ -26,6 +27,8 @@ function parseArgs(): Args {
26
27
  args.address = process.argv[++i].toLowerCase();
27
28
  } else if (arg === '--verbose') {
28
29
  args.verbose = true;
30
+ } else if (arg === '--json') {
31
+ args.json = true;
29
32
  } else if (arg === '--help' || arg === '-h') {
30
33
  console.log(`
31
34
  Spot Markets - View Hyperliquid spot markets and balances
@@ -37,6 +40,7 @@ Options:
37
40
  --balances Show your spot token balances
38
41
  --address <0x...> Look up another account's spot balances (with --balances)
39
42
  --top <n> Show only top N markets by volume
43
+ --json Output as JSON (machine-readable)
40
44
  --verbose Show detailed output
41
45
  --help Show this help
42
46
 
@@ -85,10 +89,15 @@ async function main() {
85
89
  // Show balances
86
90
  if (args.balances) {
87
91
  const lookupAddress = args.address ?? client.address;
88
- console.log(`Fetching spot balances for ${lookupAddress}...\n`);
92
+ if (!args.json) console.log(`Fetching spot balances for ${lookupAddress}...\n`);
89
93
 
90
94
  const balances = await client.getSpotBalances(args.address);
91
95
 
96
+ if (args.json) {
97
+ console.log(JSON.stringify(balances.balances ?? [], null, 2));
98
+ return;
99
+ }
100
+
92
101
  if (!balances.balances || balances.balances.length === 0) {
93
102
  console.log('No spot token balances found.');
94
103
  return;
@@ -112,17 +121,20 @@ async function main() {
112
121
  }
113
122
 
114
123
  // Show markets
115
- console.log('Fetching spot market data...\n');
124
+ if (!args.json) console.log('Fetching spot market data...\n');
116
125
 
117
126
  const spotData = await client.getSpotMetaAndAssetCtxs();
118
127
 
119
128
  interface SpotMarket {
120
129
  name: string;
121
130
  index: number;
131
+ assetId: number;
122
132
  price: string;
123
133
  volume24h: number;
124
134
  change24h: string;
125
135
  tokens: [number, number];
136
+ base?: string;
137
+ quote?: string;
126
138
  }
127
139
 
128
140
  const markets: SpotMarket[] = [];
@@ -155,10 +167,13 @@ async function main() {
155
167
  markets.push({
156
168
  name: pair.name,
157
169
  index: pair.index,
170
+ assetId: 10000 + pair.index,
158
171
  price: ctx.markPx,
159
172
  volume24h: parseFloat(ctx.dayNtlVlm || '0'),
160
173
  change24h: formatChange(ctx.markPx, ctx.prevDayPx),
161
174
  tokens: pair.tokens,
175
+ base: tokenNameMap.get(pair.tokens[0]),
176
+ quote: tokenNameMap.get(pair.tokens[1]),
162
177
  });
163
178
  }
164
179
 
@@ -168,6 +183,11 @@ async function main() {
168
183
  // Apply top filter
169
184
  const displayMarkets = args.top ? markets.slice(0, args.top) : markets;
170
185
 
186
+ if (args.json) {
187
+ console.log(JSON.stringify(displayMarkets, null, 2));
188
+ return;
189
+ }
190
+
171
191
  if (displayMarkets.length === 0) {
172
192
  console.log(args.coin ? `No spot markets found for "${args.coin}"` : 'No spot markets found');
173
193
  return;
@@ -180,8 +200,8 @@ async function main() {
180
200
  }
181
201
 
182
202
  console.log(`=== Spot Markets (${displayMarkets.length} total) ===\n`);
183
- console.log('Pair Price 24h Volume 24h Change Base/Quote');
184
- console.log('-'.repeat(80));
203
+ console.log('Pair AssetID Price 24h Volume 24h Change Base/Quote');
204
+ console.log('-'.repeat(92));
185
205
 
186
206
  for (const m of displayMarkets) {
187
207
  const baseToken = tokenMap.get(m.tokens[0]);
@@ -189,7 +209,7 @@ async function main() {
189
209
  const pairStr = `${baseToken?.name || '?'}/${quoteToken?.name || '?'}`;
190
210
 
191
211
  console.log(
192
- `${m.name.padEnd(14)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${m.change24h.padStart(11)} ${pairStr}`
212
+ `${m.name.padEnd(14)} ${String(m.assetId).padStart(7)} ${formatPrice(m.price).padStart(16)} ${formatVolume(m.volume24h).padStart(13)} ${m.change24h.padStart(11)} ${pairStr}`
193
213
  );
194
214
  }
195
215
 
@@ -11,6 +11,7 @@ Usage: openbroker trades --coin <symbol> [options]
11
11
  Options:
12
12
  --coin <symbol> Asset symbol (required, e.g. ETH, BTC)
13
13
  --top <n> Show last N trades (default: 30)
14
+ --json Output as JSON (machine-readable)
14
15
  --help, -h Show this help
15
16
 
16
17
  Examples:
@@ -35,10 +36,13 @@ async function main() {
35
36
  }
36
37
 
37
38
  const top = parseInt(args.top as string) || 30;
39
+ const jsonOutput = args.json as boolean;
38
40
  const client = getClient();
39
41
 
40
- console.log(`Open Broker - ${normalizeCoin(coin)} Recent Trades`);
41
- console.log('='.repeat(40) + '\n');
42
+ if (!jsonOutput) {
43
+ console.log(`Open Broker - ${normalizeCoin(coin)} Recent Trades`);
44
+ console.log('='.repeat(40) + '\n');
45
+ }
42
46
 
43
47
  try {
44
48
  // Load metadata (needed for HIP-3 coin resolution)
@@ -53,6 +57,13 @@ async function main() {
53
57
  trades.sort((a, b) => b.time - a.time);
54
58
  trades = trades.slice(0, top);
55
59
 
60
+ if (jsonOutput) {
61
+ let assetId = -1;
62
+ try { assetId = client.getAssetIndex(normalizeCoin(coin)); } catch { /* noop */ }
63
+ console.log(JSON.stringify({ coin: normalizeCoin(coin), assetId, trades }, null, 2));
64
+ return;
65
+ }
66
+
56
67
  if (trades.length === 0) {
57
68
  console.log('No recent trades found');
58
69
  return;
@@ -129,7 +129,7 @@ async function main() {
129
129
  console.log('\nResult');
130
130
  console.log('------');
131
131
 
132
- if (response.status === 'ok' && response.response) {
132
+ if (response.status === 'ok' && response.response && typeof response.response !== 'string') {
133
133
  const statuses = response.response.data.statuses;
134
134
  for (const status of statuses) {
135
135
  if (status.filled) {
@@ -49,11 +49,13 @@ async function main() {
49
49
 
50
50
  const response = await client.twapCancel(coin, twapId);
51
51
 
52
+ // SDK returns a success-only response shape; failures throw and are
53
+ // caught by the surrounding try/catch. `status` is the success literal.
52
54
  const status = response.response.data.status;
53
55
  if (typeof status === 'string' && status === 'success') {
54
56
  console.log(`\nTWAP order ${twapId} cancelled successfully.`);
55
- } else if (typeof status === 'object' && 'error' in status) {
56
- console.error(`\nFailed to cancel TWAP: ${status.error}`);
57
+ } else {
58
+ console.error(`\nUnexpected TWAP cancel response status: ${JSON.stringify(status)}`);
57
59
  process.exit(1);
58
60
  }
59
61
  } catch (error) {
@@ -123,17 +123,15 @@ async function main() {
123
123
  leverage,
124
124
  );
125
125
 
126
+ // SDK's TwapOrderSuccessResponse excludes the error variant — the SDK
127
+ // throws on failure and the surrounding try/catch handles that. So
128
+ // `status` only ever carries `{ running }` here.
126
129
  const status = response.response.data.status;
127
- if ('running' in status) {
128
- console.log(`TWAP order placed successfully!`);
129
- console.log(`TWAP ID: ${status.running.twapId}`);
130
- console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
131
- console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
132
- console.log(`To check status: openbroker twap-status`);
133
- } else if ('error' in status) {
134
- console.error(`TWAP order failed: ${status.error}`);
135
- process.exit(1);
136
- }
130
+ console.log(`TWAP order placed successfully!`);
131
+ console.log(`TWAP ID: ${status.running.twapId}`);
132
+ console.log(`\nThe exchange is now executing your TWAP order over ${formatDuration(durationMinutes * 60)}.`);
133
+ console.log(`To cancel: openbroker twap-cancel --coin ${coin} --twap-id ${status.running.twapId}`);
134
+ console.log(`To check status: openbroker twap-status`);
137
135
  } catch (error) {
138
136
  console.error('Error:', error instanceof Error ? error.message : error);
139
137
  process.exit(1);
@@ -1385,24 +1385,21 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1385
1385
  const midPrice = parseFloat(mids[coin]);
1386
1386
 
1387
1387
  const response = await client.twapOrder(coin, isBuy, size, durationMinutes, randomize, reduceOnly, leverage);
1388
+ // SDK's TwapOrderSuccessResponse excludes errors; they'd throw and
1389
+ // be caught below. Status is always `{ running }` on this branch.
1388
1390
  const status = response.response.data.status;
1389
1391
 
1390
- if ('running' in status) {
1391
- return json({
1392
- twapId: status.running.twapId,
1393
- coin,
1394
- side: isBuy ? 'buy' : 'sell',
1395
- size,
1396
- durationMinutes,
1397
- randomize,
1398
- reduceOnly,
1399
- estimatedNotional: midPrice ? midPrice * size : undefined,
1400
- midPrice: midPrice || undefined,
1401
- });
1402
- } else if ('error' in status) {
1403
- return error(status.error);
1404
- }
1405
- return error('Unexpected response');
1392
+ return json({
1393
+ twapId: status.running.twapId,
1394
+ coin,
1395
+ side: isBuy ? 'buy' : 'sell',
1396
+ size,
1397
+ durationMinutes,
1398
+ randomize,
1399
+ reduceOnly,
1400
+ estimatedNotional: midPrice ? midPrice * size : undefined,
1401
+ midPrice: midPrice || undefined,
1402
+ });
1406
1403
  } catch (err) {
1407
1404
  return error(err instanceof Error ? err.message : String(err));
1408
1405
  }
@@ -1428,14 +1425,13 @@ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext)
1428
1425
  try {
1429
1426
  const client = getClient();
1430
1427
  const response = await client.twapCancel(coin, twapId);
1428
+ // SDK returns the success-only response; failures throw.
1431
1429
  const status = response.response.data.status;
1432
1430
 
1433
1431
  if (typeof status === 'string' && status === 'success') {
1434
1432
  return json({ cancelled: true, coin, twapId });
1435
- } else if (typeof status === 'object' && 'error' in status) {
1436
- return error(status.error);
1437
1433
  }
1438
- return error('Unexpected response');
1434
+ return error(`Unexpected TWAP cancel status: ${JSON.stringify(status)}`);
1439
1435
  } catch (err) {
1440
1436
  return error(err instanceof Error ? err.message : String(err));
1441
1437
  }
@@ -387,8 +387,10 @@ Examples:
387
387
  return setupApiWallet();
388
388
  }
389
389
 
390
- // Options 1 & 2: Master wallet flow
391
- let privateKey: `0x${string}`;
390
+ // Options 1 & 2: Master wallet flow. Initialised in the branches below —
391
+ // the compiler can't prove the while-loop in option 2 assigns, so we use
392
+ // `| undefined` and narrow at use sites (`assertDefined`).
393
+ let privateKey: `0x${string}` | undefined;
392
394
 
393
395
  if (choice === '2') {
394
396
  // User has existing key
@@ -417,6 +419,10 @@ Examples:
417
419
  console.log('✅ New wallet created');
418
420
  }
419
421
 
422
+ if (!privateKey) {
423
+ throw new Error('Internal error: privateKey was not set by onboarding flow.');
424
+ }
425
+
420
426
  // Derive account from private key
421
427
  const account = privateKeyToAccount(privateKey);
422
428
  console.log(`\nWallet Address: ${account.address}\n`);