@bankr/cli 0.3.13 → 0.3.23

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.
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { getPortfolio, } from "../lib/api.js";
3
3
  import * as output from "../lib/output.js";
4
4
  import { formatUsd, formatBalance } from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  import { CHAIN_LABELS, VALID_CHAINS } from "../lib/chains.js";
6
7
  const BRAND = chalk.hex("#FF613D");
7
8
  const DIM = chalk.dim;
@@ -78,7 +79,7 @@ export async function portfolioCommand(opts) {
78
79
  spin.succeed("Portfolio loaded");
79
80
  }
80
81
  catch (err) {
81
- spin.fail(`Failed to fetch portfolio: ${err.message}`);
82
+ spin.fail(`Failed to fetch portfolio: ${getErrorMessage(err)}`);
82
83
  process.exit(1);
83
84
  }
84
85
  // JSON output mode
@@ -109,7 +110,7 @@ export async function portfolioCommand(opts) {
109
110
  printChainBalances(chain, chainData, !!includePnl);
110
111
  }
111
112
  // Print NFTs
112
- if (includeNfts && data.nfts && data.nfts.length > 0) {
113
+ if (includeNfts && data.nfts?.length) {
113
114
  console.log();
114
115
  console.log();
115
116
  console.log(` ${BRAND.bold("NFTs")} ${DIM(`${data.nfts.length} item(s)`)}`);
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import { input, confirm, select } from "@inquirer/prompts";
3
3
  import { getOwnProfiles, createProfile, updateProfile, deleteProfile, addProjectUpdate, } from "../lib/api.js";
4
4
  import * as output from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  const BRAND = chalk.hex("#FF613D");
6
7
  const DIM = chalk.dim;
7
8
  function printProfile(profile, json) {
@@ -74,7 +75,7 @@ export async function profileViewCommand(opts) {
74
75
  }
75
76
  }
76
77
  catch (err) {
77
- const msg = err.message;
78
+ const msg = getErrorMessage(err);
78
79
  if (msg.includes("404")) {
79
80
  output.info("No profiles found. Create one with: bankr profile create");
80
81
  }
@@ -132,7 +133,7 @@ export async function profileCreateCommand(opts) {
132
133
  printProfile(profile, !!opts.json);
133
134
  }
134
135
  catch (err) {
135
- output.error(err.message);
136
+ output.error(getErrorMessage(err));
136
137
  }
137
138
  }
138
139
  export async function profileUpdateCommand(opts) {
@@ -163,7 +164,7 @@ export async function profileUpdateCommand(opts) {
163
164
  printProfile(profile, !!opts.json);
164
165
  }
165
166
  catch (err) {
166
- output.error(err.message);
167
+ output.error(getErrorMessage(err));
167
168
  }
168
169
  }
169
170
  export async function profileDeleteCommand(opts) {
@@ -187,7 +188,7 @@ export async function profileDeleteCommand(opts) {
187
188
  output.success("Profile deleted.");
188
189
  }
189
190
  catch (err) {
190
- output.error(err.message);
191
+ output.error(getErrorMessage(err));
191
192
  }
192
193
  }
193
194
  export async function profileAddUpdateCommand(opts) {
@@ -221,7 +222,7 @@ export async function profileAddUpdateCommand(opts) {
221
222
  }
222
223
  }
223
224
  catch (err) {
224
- output.error(err.message);
225
+ output.error(getErrorMessage(err));
225
226
  }
226
227
  }
227
228
  //# sourceMappingURL=profile.js.map
@@ -2,11 +2,13 @@ import { ApiError, pollJob, submitPrompt } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import { getLastThreadId, requireApiKey, setLastThreadId, } from "../lib/config.js";
4
4
  import * as output from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  /** Valid Max Mode model IDs — hardcoded because CLI is standalone (no monorepo imports).
6
7
  * Must be manually kept in sync with models in the LLM gateway DB. */
7
8
  const VALID_MAX_MODE_MODELS = new Set([
8
9
  // Claude (Vertex AI)
9
10
  "claude-fable-5",
11
+ "claude-opus-5",
10
12
  "claude-opus-4.8",
11
13
  "claude-opus-4.7",
12
14
  "claude-opus-4.6",
@@ -15,28 +17,39 @@ const VALID_MAX_MODE_MODELS = new Set([
15
17
  "claude-sonnet-4.6",
16
18
  "claude-sonnet-4.5",
17
19
  "claude-haiku-4.5",
18
- // Gemini (Vertex AI)
20
+ // Gemini (Vertex AI) — gemini-3-pro deliberately absent: removed 2026-03-09,
21
+ // both provider slugs retired. Don't re-add on a sync pass.
22
+ "gemini-3.6-flash",
23
+ "gemini-3.5-flash-lite",
19
24
  "gemini-3.1-pro",
20
- "gemini-3-pro",
21
25
  "gemini-2.5-pro",
22
26
  // OpenAI (OpenRouter)
27
+ "gpt-5.6-sol",
28
+ "gpt-5.6-terra",
29
+ "gpt-5.6-luna",
30
+ "gpt-5.5",
23
31
  "gpt-5.4",
24
32
  "gpt-5.4-mini",
25
33
  "gpt-5.2",
26
34
  "gpt-5-mini",
27
35
  // Grok (OpenRouter)
28
- "grok-4.1-fast",
36
+ "grok-4.6",
37
+ "grok-4.5",
38
+ "grok-4.3",
29
39
  // DeepSeek (OpenRouter)
30
40
  "deepseek-v3.2",
31
41
  // Qwen (OpenRouter)
32
42
  "qwen3-coder",
43
+ "qwen3.8-max",
33
44
  "qwen3.7-max",
34
45
  "qwen3.7-plus",
46
+ "qwen3.7-flash",
35
47
  "qwen3.5-plus",
36
48
  // MiniMax (direct)
37
49
  "minimax-m2.5",
38
50
  "minimax-m2.7",
39
51
  // Other (OpenRouter)
52
+ "kimi-k3",
40
53
  "kimi-k2.7-code",
41
54
  "kimi-k2.6",
42
55
  "kimi-k2.5",
@@ -95,7 +108,7 @@ export async function promptCommand(text, options = {}) {
95
108
  }
96
109
  }
97
110
  else {
98
- output.error(err.message);
111
+ output.error(getErrorMessage(err));
99
112
  }
100
113
  process.exit(1);
101
114
  }
@@ -121,7 +134,7 @@ export async function promptCommand(text, options = {}) {
121
134
  if (result.response) {
122
135
  console.log(result.response);
123
136
  }
124
- if (result.transactions && result.transactions.length > 0) {
137
+ if (result.transactions?.length) {
125
138
  console.log();
126
139
  output.dim(`${result.transactions.length} transaction(s) returned. Use the web app to review and execute.`);
127
140
  }
@@ -141,7 +154,7 @@ export async function promptCommand(text, options = {}) {
141
154
  catch (err) {
142
155
  spin.fail("Error while polling");
143
156
  emitCESP("resource.limit");
144
- output.error(err.message);
157
+ output.error(getErrorMessage(err));
145
158
  process.exit(1);
146
159
  }
147
160
  }
@@ -1,6 +1,7 @@
1
1
  import { sign } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  export async function signCommand(opts) {
5
6
  const request = {
6
7
  signatureType: opts.type,
@@ -63,7 +64,7 @@ export async function signCommand(opts) {
63
64
  catch (err) {
64
65
  spinner.stop();
65
66
  emitCESP("task.error");
66
- output.error(err.message);
67
+ output.error(getErrorMessage(err));
67
68
  process.exit(1);
68
69
  }
69
70
  }
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { readConfig, writeConfig } from "../lib/config.js";
7
7
  import { DEFAULT_SOUND_CONFIG, emitCESP, getPacksDir, listInstalledPacks, } from "../lib/cesp/engine.js";
8
8
  import * as output from "../lib/output.js";
9
+ import { getErrorMessage } from "../lib/errors.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  const REGISTRY_URL = "https://peonping.github.io/registry/index.json";
11
12
  function getSoundConfig() {
@@ -164,7 +165,7 @@ export async function soundsInstallCommand(packName) {
164
165
  }
165
166
  catch (err) {
166
167
  spin.fail("Failed to fetch registry");
167
- output.error(err.message);
168
+ output.error(getErrorMessage(err));
168
169
  process.exit(1);
169
170
  }
170
171
  // Download and extract
@@ -218,7 +219,7 @@ export async function soundsInstallCommand(packName) {
218
219
  }
219
220
  catch (err) {
220
221
  spin2.fail("Install failed");
221
- output.error(err.message);
222
+ output.error(getErrorMessage(err));
222
223
  process.exit(1);
223
224
  }
224
225
  }
@@ -232,7 +233,7 @@ export async function soundsSearchCommand(query) {
232
233
  }
233
234
  catch (err) {
234
235
  spin.fail("Failed to fetch registry");
235
- output.error(err.message);
236
+ output.error(getErrorMessage(err));
236
237
  process.exit(1);
237
238
  }
238
239
  // Filter by query if provided
@@ -1,6 +1,7 @@
1
1
  import { getJobStatus } from "../lib/api.js";
2
2
  import { requireApiKey } from "../lib/config.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  export async function statusCommand(jobId) {
5
6
  requireApiKey();
6
7
  const spin = output.spinner("Fetching job status...");
@@ -31,14 +32,14 @@ export async function statusCommand(jobId) {
31
32
  console.log();
32
33
  output.error(status.error);
33
34
  }
34
- if (status.transactions && status.transactions.length > 0) {
35
+ if (status.transactions?.length) {
35
36
  console.log();
36
37
  output.dim(`${status.transactions.length} transaction(s) attached.`);
37
38
  }
38
39
  }
39
40
  catch (err) {
40
41
  spin.fail("Failed to fetch job status");
41
- output.error(err.message);
42
+ output.error(getErrorMessage(err));
42
43
  process.exit(1);
43
44
  }
44
45
  }
@@ -1,6 +1,7 @@
1
1
  import { submit } from "../lib/api.js";
2
2
  import { emitCESP } from "../lib/cesp/engine.js";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  function displayResult(result) {
5
6
  if (!result.success) {
6
7
  emitCESP("task.error");
@@ -73,7 +74,7 @@ export async function submitCommand(opts) {
73
74
  }
74
75
  catch (err) {
75
76
  spinner.stop();
76
- output.error(err.message);
77
+ output.error(getErrorMessage(err));
77
78
  process.exit(1);
78
79
  }
79
80
  }
@@ -127,7 +128,7 @@ export async function submitJsonCommand(json, opts) {
127
128
  }
128
129
  catch (err) {
129
130
  spinner.stop();
130
- output.error(err.message);
131
+ output.error(getErrorMessage(err));
131
132
  process.exit(1);
132
133
  }
133
134
  }
@@ -1,7 +1,9 @@
1
1
  import { getSwapQuote, executeSwap } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  import { NATIVE_SYMBOLS, VALID_CHAINS } from "../lib/chains.js";
4
- import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ import { zeroAddress } from "viem";
6
+ import { resolveTokenSymbol } from "../lib/tokens.js";
5
7
  /**
6
8
  * Resolve a token symbol or address to a contract address for swapping.
7
9
  * - The chain's native ticker (ETH/POL/BNB) or the zero address → native, sent
@@ -12,10 +14,10 @@ async function resolveSwapToken(tokenInput, chain) {
12
14
  const input = tokenInput.trim();
13
15
  const nativeSymbol = NATIVE_SYMBOLS[chain] ?? "ETH";
14
16
  const upper = input.toUpperCase();
15
- const isNative = upper === nativeSymbol.toUpperCase() ||
16
- input.toLowerCase() === ZERO_ADDRESS;
17
+ // viem's zeroAddress is already lowercase, so compare against the lowercased input.
18
+ const isNative = upper === nativeSymbol.toUpperCase() || input.toLowerCase() === zeroAddress;
17
19
  if (isNative) {
18
- return { address: ZERO_ADDRESS, symbol: nativeSymbol };
20
+ return { address: zeroAddress, symbol: nativeSymbol };
19
21
  }
20
22
  return resolveTokenSymbol(input, chain);
21
23
  }
@@ -45,7 +47,7 @@ export async function swapCommand(opts) {
45
47
  resolveSpin.stop();
46
48
  }
47
49
  catch (err) {
48
- resolveSpin.fail(err.message);
50
+ resolveSpin.fail(getErrorMessage(err));
49
51
  process.exit(1);
50
52
  }
51
53
  // Quote
@@ -62,7 +64,7 @@ export async function swapCommand(opts) {
62
64
  quoteSpin.stop();
63
65
  }
64
66
  catch (err) {
65
- quoteSpin.fail(`Quote failed: ${err.message}`);
67
+ quoteSpin.fail(`Quote failed: ${getErrorMessage(err)}`);
66
68
  process.exit(1);
67
69
  }
68
70
  output.blank();
@@ -100,7 +102,7 @@ export async function swapCommand(opts) {
100
102
  }
101
103
  }
102
104
  catch (err) {
103
- swapSpin.fail(`Swap failed: ${err.message}`);
105
+ swapSpin.fail(`Swap failed: ${getErrorMessage(err)}`);
104
106
  process.exit(1);
105
107
  }
106
108
  }
@@ -1,5 +1,6 @@
1
1
  import { searchTokens } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  export async function tokensSearchCommand(query, opts) {
4
5
  const spin = output.spinner(`Searching tokens for "${query}"...`);
5
6
  try {
@@ -22,7 +23,7 @@ export async function tokensSearchCommand(query, opts) {
22
23
  }
23
24
  }
24
25
  catch (err) {
25
- spin.fail(`Search failed: ${err.message}`);
26
+ spin.fail(`Search failed: ${getErrorMessage(err)}`);
26
27
  process.exit(1);
27
28
  }
28
29
  }
@@ -53,7 +54,7 @@ export async function tokensInfoCommand(address, opts) {
53
54
  }
54
55
  }
55
56
  catch (err) {
56
- spin.fail(`Failed to fetch token info: ${err.message}`);
57
+ spin.fail(`Failed to fetch token info: ${getErrorMessage(err)}`);
57
58
  process.exit(1);
58
59
  }
59
60
  }
@@ -1,8 +1,9 @@
1
1
  import { transfer, resolveRecipient } from "../lib/api.js";
2
2
  import * as output from "../lib/output.js";
3
+ import { getErrorMessage } from "../lib/errors.js";
3
4
  import { NATIVE_SYMBOLS } from "../lib/chains.js";
4
- import { isAddress } from "viem";
5
- import { ZERO_ADDRESS, resolveTokenSymbol } from "../lib/tokens.js";
5
+ import { isAddress, zeroAddress } from "viem";
6
+ import { resolveTokenSymbol } from "../lib/tokens.js";
6
7
  function looksLikeEnsName(value) {
7
8
  const v = value.trim().toLowerCase();
8
9
  return v.endsWith(".eth") || v.endsWith(".cb.id");
@@ -30,7 +31,7 @@ async function resolveTransferRecipient(raw) {
30
31
  };
31
32
  }
32
33
  catch (err) {
33
- spin.fail(`ENS resolution failed for "${value}": ${err.message}`);
34
+ spin.fail(`ENS resolution failed for "${value}": ${getErrorMessage(err)}`);
34
35
  process.exit(1);
35
36
  }
36
37
  }
@@ -47,7 +48,8 @@ export async function transferCommand(opts) {
47
48
  let tokenAddress;
48
49
  let tokenLabel;
49
50
  if (isNative) {
50
- tokenAddress = ZERO_ADDRESS;
51
+ // Zero address = native-token sentinel; the API detects + sanitizes it.
52
+ tokenAddress = zeroAddress;
51
53
  tokenLabel = NATIVE_SYMBOLS[chain] ?? "ETH";
52
54
  }
53
55
  else {
@@ -64,7 +66,7 @@ export async function transferCommand(opts) {
64
66
  }
65
67
  }
66
68
  catch (err) {
67
- spin.fail(err.message);
69
+ spin.fail(getErrorMessage(err));
68
70
  process.exit(1);
69
71
  }
70
72
  }
@@ -86,7 +88,7 @@ export async function transferCommand(opts) {
86
88
  output.label("Tx Hash", result.txHash ?? "unknown");
87
89
  }
88
90
  catch (err) {
89
- spin.fail(`Transfer failed: ${err.message}`);
91
+ spin.fail(`Transfer failed: ${getErrorMessage(err)}`);
90
92
  process.exit(1);
91
93
  }
92
94
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { execSync } from "node:child_process";
3
3
  import * as output from "../lib/output.js";
4
+ import { getErrorMessage } from "../lib/errors.js";
4
5
  const PACKAGE_NAME = "@bankr/cli";
5
6
  /**
6
7
  * Compare two semver version strings.
@@ -79,7 +80,7 @@ export async function updateCommand(opts) {
79
80
  }
80
81
  catch (err) {
81
82
  spin.fail();
82
- output.error(err.message);
83
+ output.error(getErrorMessage(err));
83
84
  process.exit(1);
84
85
  }
85
86
  const cmp = compareSemver(current, latest);
@@ -508,7 +508,9 @@ export async function webhooksListCommand() {
508
508
  const status = w.status === "active"
509
509
  ? output.fmt.success("active")
510
510
  : output.fmt.warn(w.status);
511
- const mode = w.readOnly ? output.fmt.dim("read-only") : output.fmt.warn("writes");
511
+ const mode = w.readOnly
512
+ ? output.fmt.dim("read-only")
513
+ : output.fmt.warn("writes");
512
514
  console.log(` ${output.fmt.brand(w.name)} ${status} v${w.version} ${mode} ${w.totalInvocations} invocations`);
513
515
  if (w.description)
514
516
  output.dim(` ${w.description}`);
@@ -564,7 +566,9 @@ export async function webhooksDeleteCommand(name) {
564
566
  export async function webhooksLogsCommand(name) {
565
567
  const spin = output.spinner(`Fetching invocations for ${name}...`);
566
568
  try {
567
- const res = await fetch(`${getApiUrl()}/webhooks/${name}/logs?limit=25`, { headers: authHeaders() });
569
+ const res = await fetch(`${getApiUrl()}/webhooks/${name}/logs?limit=25`, {
570
+ headers: authHeaders(),
571
+ });
568
572
  const result = await handleResponse(res);
569
573
  spin.stop();
570
574
  if (result.logs.length === 0) {
@@ -2,6 +2,7 @@ import { existsSync } from "fs";
2
2
  import { getApiKey, getApiUrl, getConfigPath, isCustomApiUrl, } from "../lib/config.js";
3
3
  import { getUserInfo } from "../lib/api.js";
4
4
  import * as output from "../lib/output.js";
5
+ import { getErrorMessage } from "../lib/errors.js";
5
6
  export async function whoamiCommand() {
6
7
  const apiKey = getApiKey();
7
8
  const apiUrl = getApiUrl();
@@ -35,7 +36,7 @@ export async function whoamiCommand() {
35
36
  }
36
37
  }
37
38
  catch (err) {
38
- spin.fail(`Could not reach Bankr API: ${err.message}`);
39
+ spin.fail(`Could not reach Bankr API: ${getErrorMessage(err)}`);
39
40
  return;
40
41
  }
41
42
  // Fetch user profile info
@@ -69,7 +70,7 @@ export async function whoamiCommand() {
69
70
  output.label("Score", `${score}${rankStr}`);
70
71
  }
71
72
  catch (err) {
72
- profileSpin.fail(`Could not fetch account info: ${err.message}`);
73
+ profileSpin.fail(`Could not fetch account info: ${getErrorMessage(err)}`);
73
74
  }
74
75
  }
75
76
  //# sourceMappingURL=whoami.js.map
@@ -62,6 +62,9 @@ export declare function x402EnvUnsetCommand(key: string): Promise<void>;
62
62
  export declare function x402SchemaCommand(url: string, opts?: {
63
63
  raw?: boolean;
64
64
  }): Promise<void>;
65
+ export declare function checkAdvertisedPrice(advertisedUsd: number | null, callerCapUsd: number): string | null;
66
+ export declare function parseAdvertisedPrice(price: string, currency: string): number | null;
67
+ export declare function outboundMaxPayment(callerCapUsd: number, advertisedUsd: number | null): number;
65
68
  export declare function x402CallCommand(url: string, opts?: {
66
69
  method?: string;
67
70
  body?: string;
@@ -27,6 +27,12 @@ const CONFIG_FILENAME = "bankr.x402.json";
27
27
  function findProjectRoot() {
28
28
  return process.cwd();
29
29
  }
30
+ // Config values come from user-edited JSON cast without validation, so fields
31
+ // feeding select() defaults are plain strings; narrow them to the offered
32
+ // choices (select's default must match the literal union of choice values).
33
+ function pickDefault(value, allowed) {
34
+ return allowed.find((v) => v === value);
35
+ }
30
36
  function loadConfigFile(projectRoot) {
31
37
  const candidates = [
32
38
  join(projectRoot, CONFIG_FILENAME),
@@ -238,9 +244,7 @@ export default async function handler(req: Request): Promise<Response> {
238
244
  };
239
245
  // Add to config
240
246
  let config = loadConfigFile(root);
241
- if (!config) {
242
- config = { services: {} };
243
- }
247
+ config || (config = { services: {} });
244
248
  config.services[name] = {
245
249
  price: "0.001",
246
250
  description: `${name} service`,
@@ -258,9 +262,7 @@ export default async function handler(req: Request): Promise<Response> {
258
262
  export async function x402ConfigureCommand(name) {
259
263
  const root = findProjectRoot();
260
264
  let config = loadConfigFile(root);
261
- if (!config) {
262
- config = { services: {} };
263
- }
265
+ config || (config = { services: {} });
264
266
  const existing = config.services[name] ?? { price: "0.001" };
265
267
  const description = await input({
266
268
  message: "Service description:",
@@ -273,12 +275,14 @@ export async function x402ConfigureCommand(name) {
273
275
  theme: output.bankrTheme,
274
276
  });
275
277
  const currency = "USDC";
278
+ const networkDefault = pickDefault(existing.network, ["base"]) ?? "base";
276
279
  const network = await select({
277
280
  message: "Network:",
278
281
  choices: [{ name: "Base", value: "base" }],
279
- default: existing.network ?? "base",
282
+ default: networkDefault,
280
283
  theme: output.bankrTheme,
281
284
  });
285
+ const methodDefault = pickDefault(existing.methods?.[0], ["GET", "POST", "*"]) ?? "GET";
282
286
  const method = await select({
283
287
  message: "HTTP method:",
284
288
  choices: [
@@ -286,7 +290,7 @@ export async function x402ConfigureCommand(name) {
286
290
  { name: "POST — JSON body", value: "POST" },
287
291
  { name: "Any — accept all methods", value: "*" },
288
292
  ],
289
- default: existing.methods?.[0] ?? "GET",
293
+ default: methodDefault,
290
294
  theme: output.bankrTheme,
291
295
  });
292
296
  const paymentScheme = await select({
@@ -328,9 +332,7 @@ export async function x402DeployCommand(name) {
328
332
  }
329
333
  // Load or bootstrap config
330
334
  let config = loadConfigFile(root);
331
- if (!config) {
332
- config = { services: {} };
333
- }
335
+ config || (config = { services: {} });
334
336
  // Discover services
335
337
  const { readdirSync, statSync } = await import("node:fs");
336
338
  const entries = readdirSync(x402Dir);
@@ -999,6 +1001,32 @@ async function fetchSchemaForInteractive(url) {
999
1001
  return null;
1000
1002
  }
1001
1003
  // ── Call ──────────────────────────────────────────────────────────────
1004
+ // The caller's --max-payment is the only value ever sent as the payment cap;
1005
+ // an endpoint-advertised price may never raise it — fail closed instead.
1006
+ // Exported for tests.
1007
+ export function checkAdvertisedPrice(advertisedUsd, callerCapUsd) {
1008
+ if (advertisedUsd !== null && advertisedUsd > callerCapUsd) {
1009
+ return `Endpoint requires $${advertisedUsd} but --max-payment is $${callerCapUsd}. Re-run with a higher --max-payment (up to 10) to allow it.`;
1010
+ }
1011
+ return null;
1012
+ }
1013
+ // Advertised prices that are non-positive, unparseable, or token-denominated
1014
+ // (non-USD currency) are treated as unknown: only a USD value can safely feed
1015
+ // the USD cap clamp or the fail-closed comparison. Exported for tests.
1016
+ export function parseAdvertisedPrice(price, currency) {
1017
+ const upper = currency.toUpperCase();
1018
+ if (upper !== "USDC" && upper !== "USD")
1019
+ return null;
1020
+ const parsed = parseFloat(price);
1021
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1022
+ }
1023
+ // The cap we send: never above the caller's --max-payment, and never above the
1024
+ // advertised price the user was shown and confirmed. Exported for tests.
1025
+ export function outboundMaxPayment(callerCapUsd, advertisedUsd) {
1026
+ return advertisedUsd !== null
1027
+ ? Math.min(advertisedUsd, callerCapUsd)
1028
+ : callerCapUsd;
1029
+ }
1002
1030
  export async function x402CallCommand(url, opts = {}) {
1003
1031
  if (!url) {
1004
1032
  output.error("Usage: bankr x402 call <url> [--method POST] [--body '{}']");
@@ -1011,7 +1039,8 @@ export async function x402CallCommand(url, opts = {}) {
1011
1039
  output.error("--max-payment must be between 0 and 10 (USD)");
1012
1040
  process.exit(1);
1013
1041
  }
1014
- // Interactive mode: fetch schema and prompt for input values
1042
+ // Endpoint-advertised price. Shown at confirm time; the outbound cap is
1043
+ // clamped to min(advertised, caller's maxPaymentUsd) — see outboundMaxPayment.
1015
1044
  let resolvedPrice = null;
1016
1045
  if (opts.interactive) {
1017
1046
  const schema = await fetchSchemaForInteractive(url);
@@ -1025,7 +1054,12 @@ export async function x402CallCommand(url, opts = {}) {
1025
1054
  console.log(` ${chalk.dim("Price")} ${chalk.green(`$${schema.price} ${schema.currency}`)}`);
1026
1055
  console.log();
1027
1056
  // Use the endpoint's actual price and preferred method
1028
- resolvedPrice = parseFloat(schema.price);
1057
+ resolvedPrice = parseAdvertisedPrice(schema.price, schema.currency);
1058
+ const capError = checkAdvertisedPrice(resolvedPrice, maxPaymentUsd);
1059
+ if (capError) {
1060
+ output.error(capError);
1061
+ process.exit(1);
1062
+ }
1029
1063
  if (schema.methods.length > 0) {
1030
1064
  method = schema.methods.includes("POST") ? "POST" : schema.methods[0];
1031
1065
  }
@@ -1041,7 +1075,7 @@ export async function x402CallCommand(url, opts = {}) {
1041
1075
  : `${key} ${chalk.dim(`(${typedProp.type}, optional)`)}`;
1042
1076
  const hint = typedProp.description ?? "";
1043
1077
  // Enum fields → select prompt
1044
- if (typedProp.enum && typedProp.enum.length > 0) {
1078
+ if (typedProp.enum?.length) {
1045
1079
  const selected = await select({
1046
1080
  message: `${label}${hint ? ` — ${chalk.dim(hint)}` : ""}`,
1047
1081
  choices: typedProp.enum.map((v) => ({
@@ -1098,6 +1132,7 @@ export async function x402CallCommand(url, opts = {}) {
1098
1132
  // No schema available — fallback: prompt for method and body
1099
1133
  output.warn("No schema found for this endpoint. Falling back to manual input.");
1100
1134
  console.log();
1135
+ const manualMethodDefault = pickDefault(method, ["GET", "POST", "PUT", "DELETE"]) ?? "GET";
1101
1136
  method = await select({
1102
1137
  message: "HTTP method",
1103
1138
  choices: [
@@ -1106,7 +1141,7 @@ export async function x402CallCommand(url, opts = {}) {
1106
1141
  { name: "PUT", value: "PUT" },
1107
1142
  { name: "DELETE", value: "DELETE" },
1108
1143
  ],
1109
- default: method,
1144
+ default: manualMethodDefault,
1110
1145
  theme: output.bankrTheme,
1111
1146
  });
1112
1147
  if (method === "POST" || method === "PUT") {
@@ -1144,13 +1179,22 @@ export async function x402CallCommand(url, opts = {}) {
1144
1179
  }
1145
1180
  console.log();
1146
1181
  }
1147
- // Non-interactive: probe the endpoint to show the actual price before confirming
1148
- if (!opts.interactive && !opts.yes && resolvedPrice === null) {
1182
+ // Probe the endpoint to show the actual price before confirming. Skipped in
1183
+ // non-interactive mode, where the confirm is suppressed — --ni matches --yes.
1184
+ if (!opts.interactive &&
1185
+ !opts.yes &&
1186
+ !output.isNonInteractive() &&
1187
+ resolvedPrice === null) {
1149
1188
  const spin = output.spinner("Checking endpoint price...");
1150
1189
  const schema = await fetchSchemaForInteractive(url);
1151
1190
  spin.stop();
1152
1191
  if (schema) {
1153
- resolvedPrice = parseFloat(schema.price);
1192
+ resolvedPrice = parseAdvertisedPrice(schema.price, schema.currency);
1193
+ const capError = checkAdvertisedPrice(resolvedPrice, maxPaymentUsd);
1194
+ if (capError) {
1195
+ output.error(capError);
1196
+ process.exit(1);
1197
+ }
1154
1198
  }
1155
1199
  }
1156
1200
  if (!opts.yes && !opts.interactive && !output.isNonInteractive()) {
@@ -1185,7 +1229,7 @@ export async function x402CallCommand(url, opts = {}) {
1185
1229
  url,
1186
1230
  method,
1187
1231
  body: requestBody,
1188
- maxPaymentUsd: resolvedPrice ?? maxPaymentUsd,
1232
+ maxPaymentUsd: outboundMaxPayment(maxPaymentUsd, resolvedPrice),
1189
1233
  }),
1190
1234
  });
1191
1235
  const result = await handleResponse(res);