@blockrun/mcp 0.23.1 → 0.24.0

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.
Files changed (2) hide show
  1. package/dist/index.js +492 -380
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
 
8
8
  // src/utils/wallet.ts
9
9
  import fs from "fs";
10
- import path from "path";
11
- import os from "os";
10
+ import path2 from "path";
11
+ import os2 from "os";
12
12
  import {
13
13
  LLMClient,
14
14
  ImageClient,
@@ -23,10 +23,39 @@ import {
23
23
  formatNeedsFundingMessage,
24
24
  SOLANA_WALLET_FILE_PATH
25
25
  } from "@blockrun/llm";
26
- var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
26
+
27
+ // src/utils/constants.ts
28
+ import * as path from "path";
29
+ import * as os from "os";
30
+ var WALLET_DIR = path.join(os.homedir(), ".blockrun");
31
+ var WALLET_FILE = path.join(WALLET_DIR, ".session");
32
+ var QR_FILE = path.join(WALLET_DIR, "qr.png");
33
+ var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
34
+ var BASE_CHAIN_ID = "8453";
35
+ var BASE_RPC_URLS = [
36
+ "https://mainnet.base.org",
37
+ "https://base.llamarpc.com",
38
+ "https://1rpc.io/base"
39
+ ];
40
+ var MODEL_TIERS = {
41
+ fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
42
+ balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
43
+ powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
44
+ cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
45
+ reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
46
+ // 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
47
+ // (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
48
+ // anyway; these are the free models actually serving themselves.
49
+ free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
50
+ coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
51
+ glm: ["zai/glm-5", "zai/glm-5-turbo"]
52
+ };
53
+
54
+ // src/utils/wallet.ts
55
+ var BLOCKRUN_DIR = path2.join(os2.homedir(), ".blockrun");
27
56
  var CHAIN_PREFERENCE_FILES = [
28
- path.join(BLOCKRUN_DIR, ".chain"),
29
- path.join(BLOCKRUN_DIR, "payment-chain")
57
+ path2.join(BLOCKRUN_DIR, ".chain"),
58
+ path2.join(BLOCKRUN_DIR, "payment-chain")
30
59
  ];
31
60
  var _evmClient = null;
32
61
  var _imageClient = null;
@@ -56,7 +85,7 @@ function getChain() {
56
85
  }
57
86
  return "base";
58
87
  }
59
- var CHAIN_FILE = path.join(BLOCKRUN_DIR, ".chain");
88
+ var CHAIN_FILE = path2.join(BLOCKRUN_DIR, ".chain");
60
89
  function resetChainCaches() {
61
90
  _evmClient = null;
62
91
  _solanaClient = null;
@@ -185,17 +214,15 @@ async function getSolanaUsdcBalance() {
185
214
  return null;
186
215
  }
187
216
  }
217
+ function parseBaseUsdcCallResult(raw) {
218
+ if (typeof raw !== "string" || !/^0x[0-9a-fA-F]+$/.test(raw)) return null;
219
+ return Number(BigInt(raw)) / 1e6;
220
+ }
188
221
  async function getBaseUsdcBalance(address) {
189
- const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
190
- const BASE_RPC_URLS = [
191
- "https://mainnet.base.org",
192
- "https://base.llamarpc.com",
193
- "https://1rpc.io/base"
194
- ];
195
222
  const data = {
196
223
  jsonrpc: "2.0",
197
224
  method: "eth_call",
198
- params: [{ to: USDC_ADDRESS2, data: `0x70a08231000000000000000000000000${address.slice(2)}` }, "latest"],
225
+ params: [{ to: USDC_ADDRESS, data: `0x70a08231000000000000000000000000${address.slice(2)}` }, "latest"],
199
226
  id: 1
200
227
  };
201
228
  for (const rpcUrl of BASE_RPC_URLS) {
@@ -206,7 +233,8 @@ async function getBaseUsdcBalance(address) {
206
233
  body: JSON.stringify(data)
207
234
  });
208
235
  const result = await response.json();
209
- if (result.result) return parseInt(result.result, 16) / 1e6;
236
+ const usd = parseBaseUsdcCallResult(result.result);
237
+ if (usd !== null) return usd;
210
238
  } catch {
211
239
  continue;
212
240
  }
@@ -255,6 +283,25 @@ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
255
283
  }
256
284
  return { allowed: true };
257
285
  }
286
+ function reserveBudget(budget, agentId, estimatedCost = 1e-3) {
287
+ const check = checkBudget(budget, agentId, estimatedCost);
288
+ if (!check.allowed) return { allowed: false, reason: check.reason, release: () => {
289
+ } };
290
+ const cost = Math.max(0, estimatedCost);
291
+ budget.spent += cost;
292
+ const agentBudget = agentId ? budget.agents.get(agentId) : void 0;
293
+ if (agentBudget) agentBudget.spent += cost;
294
+ let released = false;
295
+ return {
296
+ allowed: true,
297
+ release: () => {
298
+ if (released) return;
299
+ released = true;
300
+ budget.spent -= cost;
301
+ if (agentBudget) agentBudget.spent -= cost;
302
+ }
303
+ };
304
+ }
258
305
  function recordSpending(budget, cost, agentId) {
259
306
  budget.spent += cost;
260
307
  budget.calls += 1;
@@ -289,30 +336,6 @@ import QRCode from "qrcode";
289
336
  import open from "open";
290
337
  import * as fs2 from "fs";
291
338
  import * as path3 from "path";
292
-
293
- // src/utils/constants.ts
294
- import * as path2 from "path";
295
- import * as os2 from "os";
296
- var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
297
- var WALLET_FILE = path2.join(WALLET_DIR, ".session");
298
- var QR_FILE = path2.join(WALLET_DIR, "qr.png");
299
- var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
300
- var BASE_CHAIN_ID = "8453";
301
- var MODEL_TIERS = {
302
- fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
303
- balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
304
- powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
305
- cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
306
- reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
307
- // 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
308
- // (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
309
- // anyway; these are the free models actually serving themselves.
310
- free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
311
- coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
312
- glm: ["zai/glm-5", "zai/glm-5-turbo"]
313
- };
314
-
315
- // src/utils/qr.ts
316
339
  var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
317
340
  var sharpModule;
318
341
  async function loadSharp() {
@@ -423,17 +446,27 @@ ${parts.join("\n")}`;
423
446
  }
424
447
  return base;
425
448
  }
426
- function formatError(message) {
449
+ function isPaymentRejectionError(message) {
450
+ const m = message.toLowerCase();
451
+ return m.includes("insufficient") || m.includes("balance") || m.includes("rejected");
452
+ }
453
+ function formatError(message, opts) {
427
454
  const msgLower = message.toLowerCase();
428
- const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}([^0-9]|$)`).test(msgLower);
455
+ const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}($|[^0-9.])`).test(msgLower);
429
456
  const isPaymentError = hasStatus("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !hasStatus("500");
457
+ const isModelUnavailable = msgLower.includes("not active for requested provider") || msgLower.includes("not found or not active");
430
458
  const isServerError = hasStatus("500") || msgLower.includes("api error after payment");
459
+ const altHint = opts?.altModels ? ` (e.g. ${opts.altModels})` : "";
431
460
  let errorText = `Error: ${message}`;
432
- if (isServerError) {
461
+ if (isModelUnavailable) {
462
+ errorText += `
463
+
464
+ This model is temporarily unavailable upstream` + (opts?.altModels ? `. Try a different model${altHint} \u2014 it should work right away.` : `. Try a different model, or retry shortly.`);
465
+ } else if (isServerError) {
433
466
  errorText += `
434
467
 
435
468
  This is a temporary API issue. The API may be experiencing problems.
436
- Try again in a few minutes, or use a different model (e.g., openai/gpt-4o).`;
469
+ Try again in a few minutes` + (opts?.altModels ? `, or use a different model${altHint}.` : `.`);
437
470
  } else if (isPaymentError) {
438
471
  const chain = getChain();
439
472
  const network = chain === "solana" ? "Solana" : "Base";
@@ -808,6 +841,7 @@ async function handleAnthropicNative(args) {
808
841
  temperature,
809
842
  stop,
810
843
  thinking,
844
+ responseFormat,
811
845
  budget,
812
846
  agentId,
813
847
  estimatedCost
@@ -823,6 +857,9 @@ async function handleAnthropicNative(args) {
823
857
  }
824
858
  apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
825
859
  }
860
+ if (responseFormat?.type === "json_object") {
861
+ systemParts.push("Respond with only valid JSON. Do not wrap it in markdown code fences or add any prose before or after.");
862
+ }
826
863
  if (message.trim()) apiMessages.push({ role: "user", content: message });
827
864
  if (apiMessages.length === 0) {
828
865
  return { content: [{ type: "text", text: "No message content to send." }], isError: true };
@@ -892,7 +929,6 @@ ${thinkingText}` });
892
929
  function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
893
930
  if (mode === "free") return 0;
894
931
  if (model?.startsWith("nvidia/")) return 0;
895
- if (routing === "smart" && routingProfile === "free") return 0;
896
932
  const out = Math.max(maxTokens ?? 1024, 256);
897
933
  const frontierReserve = Math.max(0.01, out / 1e6 * 20);
898
934
  if (routing === "smart") {
@@ -938,7 +974,7 @@ Run blockrun_models to see all available models with pricing.`,
938
974
  model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
939
975
  mode: z2.enum(["fast", "balanced", "powerful", "cheap", "reasoning", "free", "coding", "glm"]).optional().describe("Routing mode: glm = Zhipu GLM-5/GLM-5-Turbo ($0.001/call, great for coding), coding = GLM-5 + code models, cheap = GLM-5 + budget, free = NVIDIA only (ignored if model specified)"),
940
976
  routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
941
- routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "free" (zero cost NVIDIA), "eco" (budget), "auto" (balanced, default), "premium" (best quality) (only applies when routing: "smart")'),
977
+ routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "eco" (budget), "auto" (balanced, default), "premium" (best quality). Note: "free" maps to "auto" (the SDK dropped the free profile) and still settles a PAID model \u2014 for zero-cost generation use mode:"free" or model:"nvidia/...". Only applies when routing:"smart".'),
942
978
  system: z2.string().optional().describe("Optional system prompt"),
943
979
  max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
944
980
  temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
@@ -965,141 +1001,152 @@ Run blockrun_models to see all available models with pricing.`,
965
1001
  const llm = getClient();
966
1002
  const responseFormat = response_format ? { type: response_format } : void 0;
967
1003
  const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
968
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
969
- if (!budgetCheck.allowed) {
1004
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1005
+ if (!gate.allowed) {
970
1006
  return {
971
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
1007
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
972
1008
  isError: true
973
1009
  };
974
1010
  }
975
- if (model && isAnthropicModel(model)) {
976
- const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
977
- if (solanaBlock) {
978
- return { content: [{ type: "text", text: solanaBlock }], isError: true };
979
- }
980
- return handleAnthropicNative({
981
- client: getAnthropicClient(),
982
- model,
983
- message,
984
- system,
985
- messages,
986
- maxTokens: max_tokens,
987
- temperature,
988
- stop,
989
- thinking,
990
- budget,
991
- agentId: agent_id,
992
- estimatedCost
993
- });
994
- }
995
- if (routing === "smart") {
996
- if (!(llm instanceof LLMClient2)) {
997
- return {
998
- content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
999
- isError: true
1000
- };
1001
- }
1002
- try {
1003
- const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
1011
+ try {
1012
+ if (model && isAnthropicModel(model)) {
1013
+ const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
1014
+ if (solanaBlock) {
1015
+ return { content: [{ type: "text", text: solanaBlock }], isError: true };
1016
+ }
1017
+ return await handleAnthropicNative({
1018
+ client: getAnthropicClient(),
1019
+ model,
1020
+ message,
1004
1021
  system,
1022
+ messages,
1005
1023
  maxTokens: max_tokens,
1006
- maxOutputTokens: max_tokens,
1007
1024
  temperature,
1008
- // @blockrun/llm 2.x dropped the "free" routing profile; the gateway
1009
- // already routes to the most cost-effective model by default, so we
1010
- // omit it and let ClawRouter pick (matches the SDK upgrade path).
1011
- routingProfile: routing_profile === "free" ? void 0 : routing_profile,
1025
+ stop,
1026
+ thinking,
1012
1027
  responseFormat,
1013
- stop
1014
- }));
1015
- recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
1016
- return {
1017
- content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
1028
+ budget,
1029
+ agentId: agent_id,
1030
+ estimatedCost
1031
+ });
1032
+ }
1033
+ if (routing === "smart") {
1034
+ if (messages && messages.length > 0) {
1035
+ return {
1036
+ content: [{ type: "text", text: formatError('routing:"smart" does not support multi-turn `messages` \u2014 smart routing answers a single prompt. Send the conversation via `messages` with an explicit `model`/`mode` (no routing), or send a single `message` with routing:"smart".') }],
1037
+ isError: true
1038
+ };
1039
+ }
1040
+ if (!(llm instanceof LLMClient2)) {
1041
+ return {
1042
+ content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
1043
+ isError: true
1044
+ };
1045
+ }
1046
+ try {
1047
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
1048
+ system,
1049
+ maxTokens: max_tokens,
1050
+ maxOutputTokens: max_tokens,
1051
+ temperature,
1052
+ // @blockrun/llm 2.x dropped the "free" routing profile; the gateway
1053
+ // already routes to the most cost-effective model by default, so we
1054
+ // omit it and let ClawRouter pick (matches the SDK upgrade path).
1055
+ routingProfile: routing_profile === "free" ? void 0 : routing_profile,
1056
+ responseFormat,
1057
+ stop
1058
+ }));
1059
+ recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
1060
+ return {
1061
+ content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
1018
1062
 
1019
1063
  ${result.response}` }],
1020
- structuredContent: {
1021
- model_used: result.model,
1022
- response: result.response,
1023
- routing: result.routing
1024
- }
1025
- };
1026
- } catch (error) {
1027
- return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
1064
+ structuredContent: {
1065
+ model_used: result.model,
1066
+ response: result.response,
1067
+ routing: result.routing
1068
+ }
1069
+ };
1070
+ } catch (error) {
1071
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
1072
+ }
1028
1073
  }
1029
- }
1030
- if (messages && messages.length > 0) {
1031
- const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
1032
- const fullMessages = [
1033
- ...system ? [{ role: "system", content: system }] : [],
1034
- ...messages,
1035
- { role: "user", content: message }
1036
- ];
1037
- try {
1038
- const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
1039
- maxTokens: max_tokens,
1040
- temperature,
1041
- responseFormat,
1042
- stop
1043
- }));
1044
- const reply = result.choices?.[0]?.message?.content || "";
1045
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1046
- return {
1047
- content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
1074
+ if (messages && messages.length > 0) {
1075
+ const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
1076
+ const fullMessages = [
1077
+ ...system ? [{ role: "system", content: system }] : [],
1078
+ ...messages,
1079
+ { role: "user", content: message }
1080
+ ];
1081
+ try {
1082
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
1083
+ maxTokens: max_tokens,
1084
+ temperature,
1085
+ responseFormat,
1086
+ stop
1087
+ }));
1088
+ const reply = result.choices?.[0]?.message?.content || "";
1089
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1090
+ return {
1091
+ content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
1048
1092
 
1049
1093
  ${reply}` }],
1050
- structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
1051
- };
1052
- } catch (error) {
1053
- return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
1094
+ structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
1095
+ };
1096
+ } catch (error) {
1097
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
1098
+ }
1054
1099
  }
1055
- }
1056
- if (model) {
1057
- try {
1058
- const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
1059
- system,
1060
- maxTokens: max_tokens,
1061
- temperature,
1062
- responseFormat,
1063
- stop
1064
- }));
1065
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1066
- return { content: [{ type: "text", text: response }] };
1067
- } catch (error) {
1068
- return {
1069
- content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
1070
- isError: true
1071
- };
1100
+ if (model) {
1101
+ try {
1102
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
1103
+ system,
1104
+ maxTokens: max_tokens,
1105
+ temperature,
1106
+ responseFormat,
1107
+ stop
1108
+ }));
1109
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1110
+ return { content: [{ type: "text", text: response }] };
1111
+ } catch (error) {
1112
+ return {
1113
+ content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
1114
+ isError: true
1115
+ };
1116
+ }
1072
1117
  }
1073
- }
1074
- const routingMode = mode || "balanced";
1075
- const models = MODEL_TIERS[routingMode];
1076
- let lastError = null;
1077
- for (const m of models) {
1078
- try {
1079
- const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
1080
- system,
1081
- maxTokens: max_tokens,
1082
- temperature,
1083
- responseFormat,
1084
- stop
1085
- }));
1086
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1087
- return {
1088
- content: [{ type: "text", text: `[${m}]
1118
+ const routingMode = mode || "balanced";
1119
+ const models = MODEL_TIERS[routingMode];
1120
+ let lastError = null;
1121
+ for (const m of models) {
1122
+ try {
1123
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
1124
+ system,
1125
+ maxTokens: max_tokens,
1126
+ temperature,
1127
+ responseFormat,
1128
+ stop
1129
+ }));
1130
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1131
+ return {
1132
+ content: [{ type: "text", text: `[${m}]
1089
1133
 
1090
1134
  ${response}` }],
1091
- structuredContent: { model_used: m, response }
1092
- };
1093
- } catch (error) {
1094
- lastError = error;
1095
- continue;
1135
+ structuredContent: { model_used: m, response }
1136
+ };
1137
+ } catch (error) {
1138
+ lastError = error;
1139
+ continue;
1140
+ }
1096
1141
  }
1142
+ const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
1143
+ return {
1144
+ content: [{ type: "text", text: formatError(errorMessage) }],
1145
+ isError: true
1146
+ };
1147
+ } finally {
1148
+ gate.release();
1097
1149
  }
1098
- const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
1099
- return {
1100
- content: [{ type: "text", text: formatError(errorMessage) }],
1101
- isError: true
1102
- };
1103
1150
  }
1104
1151
  );
1105
1152
  }
@@ -1187,6 +1234,10 @@ async function toImageDataUri(ref) {
1187
1234
  if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
1188
1235
  const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
1189
1236
  if (!mime2.startsWith("image/")) throw new Error(`URL returned non-image content-type: ${mime2 || "(none)"}`);
1237
+ const advertised = Number(res.headers.get("content-length"));
1238
+ if (Number.isFinite(advertised) && advertised > REFERENCE_IMAGE_MAX_BYTES) {
1239
+ throw new Error(`image too large: ${(advertised / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
1240
+ }
1190
1241
  const buffer2 = Buffer.from(await res.arrayBuffer());
1191
1242
  if (buffer2.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
1192
1243
  throw new Error(`image too large: ${(buffer2.byteLength / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
@@ -1343,34 +1394,42 @@ Source images and masks accept a base64 data URI, an http(s) URL, or a local fil
1343
1394
  };
1344
1395
  }
1345
1396
  const estimatedCost = estimateCost(selectedModel, size);
1346
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1347
- if (!budgetCheck.allowed) {
1397
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1398
+ if (!gate.allowed) {
1348
1399
  return {
1349
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1400
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1350
1401
  isError: true
1351
1402
  };
1352
1403
  }
1353
- response = await getImageClient().edit(prompt, normalizedImage, {
1354
- model: selectedModel,
1355
- size,
1356
- ...normalizedMask ? { mask: normalizedMask } : {}
1357
- });
1358
- recordSpending(budget, estimatedCost, agent_id);
1404
+ try {
1405
+ response = await getImageClient().edit(prompt, normalizedImage, {
1406
+ model: selectedModel,
1407
+ size,
1408
+ ...normalizedMask ? { mask: normalizedMask } : {}
1409
+ });
1410
+ recordSpending(budget, estimatedCost, agent_id);
1411
+ } finally {
1412
+ gate.release();
1413
+ }
1359
1414
  } else {
1360
1415
  const estimatedCost = estimateCost(selectedModel, size);
1361
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1362
- if (!budgetCheck.allowed) {
1416
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1417
+ if (!gate.allowed) {
1363
1418
  return {
1364
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1419
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1365
1420
  isError: true
1366
1421
  };
1367
1422
  }
1368
- response = await getImageClient().generate(prompt, {
1369
- model: selectedModel,
1370
- size,
1371
- quality
1372
- });
1373
- recordSpending(budget, estimatedCost, agent_id);
1423
+ try {
1424
+ response = await getImageClient().generate(prompt, {
1425
+ model: selectedModel,
1426
+ size,
1427
+ quality
1428
+ });
1429
+ recordSpending(budget, estimatedCost, agent_id);
1430
+ } finally {
1431
+ gate.release();
1432
+ }
1374
1433
  }
1375
1434
  const imageUrl = response.data?.[0]?.url;
1376
1435
  if (!imageUrl) {
@@ -1395,7 +1454,7 @@ Error: ${errMsg}` }],
1395
1454
  };
1396
1455
  }
1397
1456
  return {
1398
- content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`) }],
1457
+ content: [{ type: "text", text: formatError(`Image generation failed: ${errMsg}`, { altModels: "google/nano-banana, zai/cogview-4" }) }],
1399
1458
  isError: true
1400
1459
  };
1401
1460
  }
@@ -1411,11 +1470,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
1411
1470
  const controller = new AbortController();
1412
1471
  const id = setTimeout(() => controller.abort(), timeoutMs);
1413
1472
  id.unref?.();
1414
- try {
1415
- return await fetch(url, { ...options, signal: controller.signal });
1416
- } finally {
1417
- clearTimeout(id);
1418
- }
1473
+ return await fetch(url, { ...options, signal: controller.signal });
1419
1474
  }
1420
1475
  function isTimeoutError(err) {
1421
1476
  const name = err instanceof Error ? err.name : "";
@@ -1454,6 +1509,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1454
1509
  }
1455
1510
  },
1456
1511
  async ({ prompt, instrumental, lyrics, model, agent_id }) => {
1512
+ let gate;
1457
1513
  try {
1458
1514
  if (getChain() !== "base") {
1459
1515
  return {
@@ -1467,10 +1523,10 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1467
1523
  isError: true
1468
1524
  };
1469
1525
  }
1470
- const budgetCheck = checkBudget(budget, agent_id, MUSIC_COST);
1471
- if (!budgetCheck.allowed) {
1526
+ gate = reserveBudget(budget, agent_id, MUSIC_COST);
1527
+ if (!gate.allowed) {
1472
1528
  return {
1473
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1529
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1474
1530
  isError: true
1475
1531
  };
1476
1532
  }
@@ -1485,8 +1541,8 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1485
1541
  body: JSON.stringify(body)
1486
1542
  }, 15e3);
1487
1543
  if (resp402.status !== 402) {
1488
- const data2 = await resp402.json();
1489
- throw new Error(`Expected 402, got ${resp402.status}: ${JSON.stringify(data2)}`);
1544
+ const data2 = await resp402.json().catch(() => ({}));
1545
+ throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
1490
1546
  }
1491
1547
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1492
1548
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -1547,7 +1603,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1547
1603
  };
1548
1604
  } catch (err) {
1549
1605
  const errMsg = err instanceof Error ? err.message : String(err);
1550
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
1606
+ if (isPaymentRejectionError(errMsg)) {
1551
1607
  return {
1552
1608
  content: [{ type: "text", text: `Music generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
1553
1609
  Error: ${errMsg}` }],
@@ -1565,6 +1621,8 @@ Error: ${errMsg}` }],
1565
1621
  content: [{ type: "text", text: formatError(`Music generation failed: ${errMsg}`) }],
1566
1622
  isError: true
1567
1623
  };
1624
+ } finally {
1625
+ gate?.release();
1568
1626
  }
1569
1627
  }
1570
1628
  );
@@ -1634,6 +1692,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1634
1692
  }
1635
1693
  },
1636
1694
  async ({ action, input, voice, model, response_format, speed, duration_seconds, prompt_influence, agent_id }) => {
1695
+ let gate;
1637
1696
  try {
1638
1697
  if (action === "voices") {
1639
1698
  return await listVoices();
@@ -1678,10 +1737,10 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1678
1737
  if (speed !== void 0) body.speed = speed;
1679
1738
  cost = speechCost(model, input.length);
1680
1739
  }
1681
- const budgetCheck = checkBudget(budget, agent_id, cost);
1682
- if (!budgetCheck.allowed) {
1740
+ gate = reserveBudget(budget, agent_id, cost);
1741
+ if (!gate.allowed) {
1683
1742
  return {
1684
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1743
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1685
1744
  isError: true
1686
1745
  };
1687
1746
  }
@@ -1694,7 +1753,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1694
1753
  }, 15e3);
1695
1754
  if (resp402.status !== 402) {
1696
1755
  const data2 = await resp402.json().catch(() => ({}));
1697
- throw new Error(`Expected 402, got ${resp402.status}: ${JSON.stringify(data2)}`);
1756
+ throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
1698
1757
  }
1699
1758
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1700
1759
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -1760,7 +1819,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1760
1819
  };
1761
1820
  } catch (err) {
1762
1821
  const errMsg = err instanceof Error ? err.message : String(err);
1763
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
1822
+ if (isPaymentRejectionError(errMsg)) {
1764
1823
  return {
1765
1824
  content: [{ type: "text", text: `Speech generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
1766
1825
  Error: ${errMsg}` }],
@@ -1778,6 +1837,8 @@ Error: ${errMsg}` }],
1778
1837
  content: [{ type: "text", text: formatError(`Speech generation failed: ${errMsg}`) }],
1779
1838
  isError: true
1780
1839
  };
1840
+ } finally {
1841
+ gate?.release();
1781
1842
  }
1782
1843
  }
1783
1844
  );
@@ -1878,6 +1939,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1878
1939
  }
1879
1940
  },
1880
1941
  async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
1942
+ let gate;
1881
1943
  try {
1882
1944
  if (getChain() !== "base") {
1883
1945
  return {
@@ -1918,10 +1980,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1918
1980
  const hasImageInput = Boolean(image_url || real_face_asset_id);
1919
1981
  const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
1920
1982
  const estimatedCost = perSecond * billedSeconds;
1921
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1922
- if (!budgetCheck.allowed) {
1983
+ gate = reserveBudget(budget, agent_id, estimatedCost);
1984
+ if (!gate.allowed) {
1923
1985
  return {
1924
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1986
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1925
1987
  isError: true
1926
1988
  };
1927
1989
  }
@@ -1942,8 +2004,8 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1942
2004
  body: JSON.stringify(body)
1943
2005
  }, 15e3);
1944
2006
  if (resp402.status !== 402) {
1945
- const data = await resp402.json();
1946
- throw new Error(`Expected 402, got ${resp402.status}: ${JSON.stringify(data)}`);
2007
+ const data = await resp402.json().catch(() => ({}));
2008
+ throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data)}`);
1947
2009
  }
1948
2010
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1949
2011
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -2047,7 +2109,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
2047
2109
  };
2048
2110
  } catch (err) {
2049
2111
  const errMsg = err instanceof Error ? err.message : String(err);
2050
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
2112
+ if (isPaymentRejectionError(errMsg)) {
2051
2113
  return {
2052
2114
  content: [{ type: "text", text: `Video generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
2053
2115
  Error: ${errMsg}` }],
@@ -2062,9 +2124,11 @@ Error: ${errMsg}` }],
2062
2124
  };
2063
2125
  }
2064
2126
  return {
2065
- content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`) }],
2127
+ content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`, { altModels: "bytedance/seedance-2.0, azure/sora-2" }) }],
2066
2128
  isError: true
2067
2129
  };
2130
+ } finally {
2131
+ gate?.release();
2068
2132
  }
2069
2133
  }
2070
2134
  );
@@ -2090,7 +2154,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
2090
2154
  }, 15e3);
2091
2155
  if (resp402.status !== 402) {
2092
2156
  const data2 = await resp402.json().catch(() => ({}));
2093
- throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
2157
+ throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${data2.message || data2.error || JSON.stringify(data2)}`);
2094
2158
  }
2095
2159
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
2096
2160
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -2151,6 +2215,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
2151
2215
  }
2152
2216
  },
2153
2217
  async ({ action, name, group_id, image_url, agent_id }) => {
2218
+ let gate;
2154
2219
  try {
2155
2220
  if (action === "init") {
2156
2221
  if (!name) {
@@ -2277,9 +2342,9 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2277
2342
  if (!name || !image_url) {
2278
2343
  return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
2279
2344
  }
2280
- const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2281
- if (!budgetCheck.allowed) {
2282
- return { content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2345
+ gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2346
+ if (!gate.allowed) {
2347
+ return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2283
2348
  }
2284
2349
  const { status, data, settledUsd } = await payAndPostJson(
2285
2350
  `${BLOCKRUN_API4}/v1/portrait/enroll`,
@@ -2327,61 +2392,26 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2327
2392
  if (!name || !image_url || !group_id) {
2328
2393
  return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
2329
2394
  }
2330
- const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2331
- if (!budgetCheck.allowed) {
2332
- return { content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2395
+ gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2396
+ if (!gate.allowed) {
2397
+ return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
2333
2398
  }
2334
- const privateKey = getOrCreateWalletKey();
2335
- const account = privateKeyToAccount4(privateKey);
2336
- const enrollUrl = `${BLOCKRUN_API4}/v1/realface/enroll`;
2337
- const reqBody = JSON.stringify({ name, image_url, group_id });
2338
- const resp402 = await fetchWithTimeout(enrollUrl, {
2339
- method: "POST",
2340
- headers: { "Content-Type": "application/json" },
2341
- body: reqBody
2342
- }, 15e3);
2343
- if (resp402.status !== 402) {
2344
- const data2 = await resp402.json().catch(() => ({}));
2345
- throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
2346
- }
2347
- const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
2348
- if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
2349
- const paymentRequired = parsePaymentRequired4(prHeader);
2350
- const details = extractPaymentDetails4(paymentRequired);
2351
- const settledUsd = amountToUsd(details.amount);
2352
- const paymentPayload = await createPaymentPayload4(
2353
- privateKey,
2354
- account.address,
2355
- details.recipient,
2356
- details.amount,
2357
- details.network || "eip155:8453",
2358
- {
2359
- resourceUrl: details.resource?.url || enrollUrl,
2360
- resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
2361
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
2362
- extra: details.extra
2363
- }
2399
+ const { status, data, settledUsd } = await payAndPostJson(
2400
+ `${BLOCKRUN_API4}/v1/realface/enroll`,
2401
+ JSON.stringify({ name, image_url, group_id }),
2402
+ "BlockRun RealFace enrollment"
2364
2403
  );
2365
- const resp = await fetchWithTimeout(enrollUrl, {
2366
- method: "POST",
2367
- headers: {
2368
- "Content-Type": "application/json",
2369
- "PAYMENT-SIGNATURE": paymentPayload
2370
- },
2371
- body: reqBody
2372
- }, 9e4);
2373
- const data = await resp.json().catch(() => ({}));
2374
- if (resp.status === 402) {
2404
+ if (status === 402) {
2375
2405
  throw new Error("Payment rejected. Check your wallet balance.");
2376
2406
  }
2377
- if (resp.status === 425) {
2407
+ if (status === 425) {
2378
2408
  return { content: [{ type: "text", text: formatError(`Group not active yet \u2014 ${data.message || "finish the phone liveness check first"}. No payment taken.`) }], isError: true };
2379
2409
  }
2380
- if (resp.status === 422) {
2410
+ if (status === 422) {
2381
2411
  return { content: [{ type: "text", text: formatError(`Face match failed \u2014 ${data.hint || "use a clearer front-facing photo of the same person"}. No payment taken.`) }], isError: true };
2382
2412
  }
2383
- if (!resp.ok) {
2384
- throw new Error(`Enroll error ${resp.status}: ${data.error || JSON.stringify(data)}`);
2413
+ if (status < 200 || status >= 300) {
2414
+ throw new Error(`Enroll error ${status}: ${data.error || JSON.stringify(data)}`);
2385
2415
  }
2386
2416
  const assetId = data.asset_id;
2387
2417
  if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
@@ -2410,7 +2440,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2410
2440
  return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
2411
2441
  } catch (err) {
2412
2442
  const errMsg = err instanceof Error ? err.message : String(err);
2413
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
2443
+ if (isPaymentRejectionError(errMsg)) {
2414
2444
  return {
2415
2445
  content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
2416
2446
  Error: ${errMsg}` }],
@@ -2418,6 +2448,8 @@ Error: ${errMsg}` }],
2418
2448
  };
2419
2449
  }
2420
2450
  return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
2451
+ } finally {
2452
+ gate?.release();
2421
2453
  }
2422
2454
  }
2423
2455
  );
@@ -2441,6 +2473,19 @@ function asStructuredContent(result) {
2441
2473
  return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
2442
2474
  }
2443
2475
 
2476
+ // src/utils/path-safety.ts
2477
+ function hasPathTraversal(path5) {
2478
+ let decoded = path5;
2479
+ try {
2480
+ decoded = decodeURIComponent(path5);
2481
+ } catch {
2482
+ }
2483
+ return decoded.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
2484
+ }
2485
+ function isValidNetworkSlug(slug) {
2486
+ return /^[a-z0-9-]+$/.test(slug);
2487
+ }
2488
+
2444
2489
  // src/tools/search.ts
2445
2490
  var SEARCH_PRICE_PER_SOURCE = 0.025;
2446
2491
  var SEARCH_DEFAULT_MAX_RESULTS = 10;
@@ -2471,23 +2516,30 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
2471
2516
  async ({ path: path5, body, agent_id }) => {
2472
2517
  try {
2473
2518
  body = coerceBody(body);
2519
+ const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
2520
+ if (hasPathTraversal(cleanPath)) {
2521
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
2522
+ }
2474
2523
  const estimatedCost = estimateSearchCost(body);
2475
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2476
- if (!budgetCheck.allowed) {
2524
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2525
+ if (!gate.allowed) {
2477
2526
  return {
2478
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2527
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2479
2528
  isError: true
2480
2529
  };
2481
2530
  }
2482
- const client = getClient();
2483
- const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
2484
- const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
2485
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2486
- recordSpending(budget, estimatedCost, agent_id);
2487
- return {
2488
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2489
- structuredContent: asStructuredContent(result)
2490
- };
2531
+ try {
2532
+ const client = getClient();
2533
+ const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
2534
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2535
+ recordSpending(budget, estimatedCost, agent_id);
2536
+ return {
2537
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2538
+ structuredContent: asStructuredContent(result)
2539
+ };
2540
+ } finally {
2541
+ gate.release();
2542
+ }
2491
2543
  } catch (err) {
2492
2544
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2493
2545
  }
@@ -2529,23 +2581,30 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
2529
2581
  async ({ path: path5, body, agent_id }) => {
2530
2582
  try {
2531
2583
  body = coerceBody(body);
2584
+ const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
2585
+ if (hasPathTraversal(cleanPath)) {
2586
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
2587
+ }
2532
2588
  const estimatedCost = estimateExaCost(path5, body);
2533
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2534
- if (!budgetCheck.allowed) {
2589
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2590
+ if (!gate.allowed) {
2535
2591
  return {
2536
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2592
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2537
2593
  isError: true
2538
2594
  };
2539
2595
  }
2540
- const client = getClient();
2541
- const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
2542
- const endpoint = `/v1/exa/${cleanPath}`;
2543
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2544
- recordSpending(budget, estimatedCost, agent_id);
2545
- return {
2546
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2547
- structuredContent: asStructuredContent(result)
2548
- };
2596
+ try {
2597
+ const client = getClient();
2598
+ const endpoint = `/v1/exa/${cleanPath}`;
2599
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2600
+ recordSpending(budget, estimatedCost, agent_id);
2601
+ return {
2602
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2603
+ structuredContent: asStructuredContent(result)
2604
+ };
2605
+ } finally {
2606
+ gate.release();
2607
+ }
2549
2608
  } catch (err) {
2550
2609
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2551
2610
  }
@@ -2628,21 +2687,28 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
2628
2687
  async ({ path: path5, params, body, agent_id }) => {
2629
2688
  try {
2630
2689
  body = coerceBody(body);
2690
+ if (hasPathTraversal(path5)) {
2691
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
2692
+ }
2631
2693
  const estimatedCost = estimateMarketCost(path5, body);
2632
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2633
- if (!budgetCheck.allowed) {
2694
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2695
+ if (!gate.allowed) {
2634
2696
  return {
2635
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2697
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2636
2698
  isError: true
2637
2699
  };
2638
2700
  }
2639
- const llm = getClient();
2640
- const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
2641
- recordSpending(budget, estimatedCost, agent_id);
2642
- return {
2643
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2644
- structuredContent: asStructuredContent(result)
2645
- };
2701
+ try {
2702
+ const llm = getClient();
2703
+ const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
2704
+ recordSpending(budget, estimatedCost, agent_id);
2705
+ return {
2706
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2707
+ structuredContent: asStructuredContent(result)
2708
+ };
2709
+ } finally {
2710
+ gate.release();
2711
+ }
2646
2712
  } catch (err) {
2647
2713
  return {
2648
2714
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -2724,51 +2790,55 @@ Examples:
2724
2790
  };
2725
2791
  }
2726
2792
  const estimatedCost = paid ? 1e-3 : 0;
2727
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2728
- if (!budgetCheck.allowed) {
2793
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2794
+ if (!gate.allowed) {
2729
2795
  return {
2730
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2796
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2731
2797
  isError: true
2732
2798
  };
2733
2799
  }
2734
- const priceClient = getPriceClient(paid);
2735
- if (action === "price") {
2736
- if (!symbol) throw new Error("symbol is required for action='price'");
2737
- const result2 = await priceClient.price(category, symbol, {
2738
- market,
2739
- session
2740
- });
2741
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2742
- return {
2743
- content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2744
- structuredContent: result2
2745
- };
2746
- }
2747
- if (action === "history") {
2748
- if (!symbol) throw new Error("symbol is required for action='history'");
2749
- if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
2750
- const result2 = await priceClient.history(category, symbol, {
2800
+ try {
2801
+ const priceClient = getPriceClient(paid);
2802
+ if (action === "price") {
2803
+ if (!symbol) throw new Error("symbol is required for action='price'");
2804
+ const result2 = await priceClient.price(category, symbol, {
2805
+ market,
2806
+ session
2807
+ });
2808
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2809
+ return {
2810
+ content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2811
+ structuredContent: result2
2812
+ };
2813
+ }
2814
+ if (action === "history") {
2815
+ if (!symbol) throw new Error("symbol is required for action='history'");
2816
+ if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
2817
+ const result2 = await priceClient.history(category, symbol, {
2818
+ market,
2819
+ session,
2820
+ resolution: resolution ?? "D",
2821
+ from,
2822
+ to
2823
+ });
2824
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2825
+ return {
2826
+ content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2827
+ structuredContent: result2
2828
+ };
2829
+ }
2830
+ const result = await priceClient.listSymbols(category, {
2751
2831
  market,
2752
- session,
2753
- resolution: resolution ?? "D",
2754
- from,
2755
- to
2832
+ query,
2833
+ limit
2756
2834
  });
2757
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2758
2835
  return {
2759
- content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2760
- structuredContent: result2
2836
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2837
+ structuredContent: result
2761
2838
  };
2839
+ } finally {
2840
+ gate.release();
2762
2841
  }
2763
- const result = await priceClient.listSymbols(category, {
2764
- market,
2765
- query,
2766
- limit
2767
- });
2768
- return {
2769
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2770
- structuredContent: result
2771
- };
2772
2842
  } catch (err) {
2773
2843
  return {
2774
2844
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -2899,22 +2969,29 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
2899
2969
  try {
2900
2970
  body = coerceBody(body);
2901
2971
  const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
2972
+ if (hasPathTraversal(cleanPath)) {
2973
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
2974
+ }
2902
2975
  const estimatedCost = estimateModalCost(cleanPath);
2903
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2904
- if (!budgetCheck.allowed) {
2976
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2977
+ if (!gate.allowed) {
2905
2978
  return {
2906
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2979
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2907
2980
  isError: true
2908
2981
  };
2909
2982
  }
2910
- const client = buildClientWithTimeout(modalTimeoutMs(body));
2911
- const endpoint = `/v1/modal/${cleanPath}`;
2912
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2913
- recordSpending(budget, estimatedCost, agent_id);
2914
- return {
2915
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2916
- structuredContent: asStructuredContent(result)
2917
- };
2983
+ try {
2984
+ const client = buildClientWithTimeout(modalTimeoutMs(body));
2985
+ const endpoint = `/v1/modal/${cleanPath}`;
2986
+ const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2987
+ recordSpending(budget, estimatedCost, agent_id);
2988
+ return {
2989
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2990
+ structuredContent: asStructuredContent(result)
2991
+ };
2992
+ } finally {
2993
+ gate.release();
2994
+ }
2918
2995
  } catch (err) {
2919
2996
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2920
2997
  }
@@ -2965,22 +3042,29 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
2965
3042
  try {
2966
3043
  body = coerceBody(body);
2967
3044
  const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
3045
+ if (hasPathTraversal(cleanPath)) {
3046
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
3047
+ }
2968
3048
  const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
2969
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2970
- if (!budgetCheck.allowed) {
3049
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3050
+ if (!gate.allowed) {
2971
3051
  return {
2972
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3052
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
2973
3053
  isError: true
2974
3054
  };
2975
3055
  }
2976
- const client = getClient();
2977
- const endpoint = `/v1/${cleanPath}`;
2978
- const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
2979
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2980
- return {
2981
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2982
- structuredContent: asStructuredContent(result)
2983
- };
3056
+ try {
3057
+ const client = getClient();
3058
+ const endpoint = `/v1/${cleanPath}`;
3059
+ const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
3060
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
3061
+ return {
3062
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3063
+ structuredContent: asStructuredContent(result)
3064
+ };
3065
+ } finally {
3066
+ gate.release();
3067
+ }
2984
3068
  } catch (err) {
2985
3069
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2986
3070
  }
@@ -3063,22 +3147,29 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
3063
3147
  try {
3064
3148
  body = coerceBody(body);
3065
3149
  const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
3150
+ if (hasPathTraversal(cleanPath)) {
3151
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
3152
+ }
3066
3153
  const estimatedCost = estimateSurfCost(cleanPath);
3067
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3068
- if (!budgetCheck.allowed) {
3154
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3155
+ if (!gate.allowed) {
3069
3156
  return {
3070
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3157
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3071
3158
  isError: true
3072
3159
  };
3073
3160
  }
3074
- const client = getClient();
3075
- const endpoint = `/v1/surf/${cleanPath}`;
3076
- const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
3077
- recordSpending(budget, estimatedCost, agent_id);
3078
- return {
3079
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3080
- structuredContent: asStructuredContent(result)
3081
- };
3161
+ try {
3162
+ const client = getClient();
3163
+ const endpoint = `/v1/surf/${cleanPath}`;
3164
+ const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
3165
+ recordSpending(budget, estimatedCost, agent_id);
3166
+ return {
3167
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3168
+ structuredContent: asStructuredContent(result)
3169
+ };
3170
+ } finally {
3171
+ gate.release();
3172
+ }
3082
3173
  } catch (err) {
3083
3174
  return {
3084
3175
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3132,21 +3223,31 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
3132
3223
  }
3133
3224
  const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
3134
3225
  const estimatedCost = RPC_PRICE_USD * batchCount;
3135
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3136
- if (!budgetCheck.allowed) {
3226
+ const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
3227
+ if (!isValidNetworkSlug(cleanNetwork)) {
3137
3228
  return {
3138
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3229
+ content: [{ type: "text", text: formatError(`Invalid network '${network}'. Use a chain slug like 'ethereum', 'base', or 'solana'.`) }],
3139
3230
  isError: true
3140
3231
  };
3141
3232
  }
3142
- const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
3143
- const client = getClient();
3144
- const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
3145
- recordSpending(budget, estimatedCost, agent_id);
3146
- return {
3147
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3148
- structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3149
- };
3233
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3234
+ if (!gate.allowed) {
3235
+ return {
3236
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3237
+ isError: true
3238
+ };
3239
+ }
3240
+ try {
3241
+ const client = getClient();
3242
+ const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
3243
+ recordSpending(budget, estimatedCost, agent_id);
3244
+ return {
3245
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3246
+ structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3247
+ };
3248
+ } finally {
3249
+ gate.release();
3250
+ }
3150
3251
  } catch (err) {
3151
3252
  return {
3152
3253
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3189,21 +3290,28 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
3189
3290
  async ({ path: path5, agent_id }) => {
3190
3291
  try {
3191
3292
  const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
3293
+ if (hasPathTraversal(cleanPath)) {
3294
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
3295
+ }
3192
3296
  const estimatedCost = estimateDefiCost(cleanPath);
3193
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3194
- if (!budgetCheck.allowed) {
3297
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3298
+ if (!gate.allowed) {
3195
3299
  return {
3196
- content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3300
+ content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
3197
3301
  isError: true
3198
3302
  };
3199
3303
  }
3200
- const client = getClient();
3201
- const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
3202
- recordSpending(budget, estimatedCost, agent_id);
3203
- return {
3204
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3205
- structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3206
- };
3304
+ try {
3305
+ const client = getClient();
3306
+ const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
3307
+ recordSpending(budget, estimatedCost, agent_id);
3308
+ return {
3309
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3310
+ structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3311
+ };
3312
+ } finally {
3313
+ gate.release();
3314
+ }
3207
3315
  } catch (err) {
3208
3316
  return {
3209
3317
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3262,7 +3370,7 @@ function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
3262
3370
  }
3263
3371
  function resolveTools(argv, env) {
3264
3372
  const requested = resolveProfileName(argv, env);
3265
- const spec = PROFILES[requested];
3373
+ const spec = Object.hasOwn(PROFILES, requested) ? PROFILES[requested] : void 0;
3266
3374
  if (!spec) {
3267
3375
  return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
3268
3376
  }
@@ -3353,6 +3461,10 @@ function looksLikeRawPrivateKey(value) {
3353
3461
  if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
3354
3462
  return false;
3355
3463
  }
3464
+ function looksLikeNamedSecretValue(value) {
3465
+ if (looksLikeRawPrivateKey(value)) return true;
3466
+ return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
3467
+ }
3356
3468
  function looksLikeSolanaSecretKeyArray(value) {
3357
3469
  return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
3358
3470
  }
@@ -3367,7 +3479,7 @@ function walk(obj, file, jsonPath, out) {
3367
3479
  }
3368
3480
  for (const [k, v] of Object.entries(obj)) {
3369
3481
  const next = jsonPath ? `${jsonPath}.${k}` : k;
3370
- if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeRawPrivateKey(v)) {
3482
+ if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeNamedSecretValue(v)) {
3371
3483
  out.push({ file, path: next });
3372
3484
  } else if (looksLikeRawPrivateKey(v)) {
3373
3485
  out.push({ file, path: next });