@blockrun/mcp 0.23.2 → 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 +481 -375
  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,9 +446,13 @@ ${parts.join("\n")}`;
423
446
  }
424
447
  return base;
425
448
  }
449
+ function isPaymentRejectionError(message) {
450
+ const m = message.toLowerCase();
451
+ return m.includes("insufficient") || m.includes("balance") || m.includes("rejected");
452
+ }
426
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");
430
457
  const isModelUnavailable = msgLower.includes("not active for requested provider") || msgLower.includes("not found or not active");
431
458
  const isServerError = hasStatus("500") || msgLower.includes("api error after payment");
@@ -814,6 +841,7 @@ async function handleAnthropicNative(args) {
814
841
  temperature,
815
842
  stop,
816
843
  thinking,
844
+ responseFormat,
817
845
  budget,
818
846
  agentId,
819
847
  estimatedCost
@@ -829,6 +857,9 @@ async function handleAnthropicNative(args) {
829
857
  }
830
858
  apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
831
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
+ }
832
863
  if (message.trim()) apiMessages.push({ role: "user", content: message });
833
864
  if (apiMessages.length === 0) {
834
865
  return { content: [{ type: "text", text: "No message content to send." }], isError: true };
@@ -898,7 +929,6 @@ ${thinkingText}` });
898
929
  function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
899
930
  if (mode === "free") return 0;
900
931
  if (model?.startsWith("nvidia/")) return 0;
901
- if (routing === "smart" && routingProfile === "free") return 0;
902
932
  const out = Math.max(maxTokens ?? 1024, 256);
903
933
  const frontierReserve = Math.max(0.01, out / 1e6 * 20);
904
934
  if (routing === "smart") {
@@ -944,7 +974,7 @@ Run blockrun_models to see all available models with pricing.`,
944
974
  model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
945
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)"),
946
976
  routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
947
- 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".'),
948
978
  system: z2.string().optional().describe("Optional system prompt"),
949
979
  max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
950
980
  temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
@@ -971,141 +1001,152 @@ Run blockrun_models to see all available models with pricing.`,
971
1001
  const llm = getClient();
972
1002
  const responseFormat = response_format ? { type: response_format } : void 0;
973
1003
  const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
974
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
975
- if (!budgetCheck.allowed) {
1004
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1005
+ if (!gate.allowed) {
976
1006
  return {
977
- 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.` }],
978
1008
  isError: true
979
1009
  };
980
1010
  }
981
- if (model && isAnthropicModel(model)) {
982
- const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
983
- if (solanaBlock) {
984
- return { content: [{ type: "text", text: solanaBlock }], isError: true };
985
- }
986
- return handleAnthropicNative({
987
- client: getAnthropicClient(),
988
- model,
989
- message,
990
- system,
991
- messages,
992
- maxTokens: max_tokens,
993
- temperature,
994
- stop,
995
- thinking,
996
- budget,
997
- agentId: agent_id,
998
- estimatedCost
999
- });
1000
- }
1001
- if (routing === "smart") {
1002
- if (!(llm instanceof LLMClient2)) {
1003
- return {
1004
- content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
1005
- isError: true
1006
- };
1007
- }
1008
- try {
1009
- 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,
1010
1021
  system,
1022
+ messages,
1011
1023
  maxTokens: max_tokens,
1012
- maxOutputTokens: max_tokens,
1013
1024
  temperature,
1014
- // @blockrun/llm 2.x dropped the "free" routing profile; the gateway
1015
- // already routes to the most cost-effective model by default, so we
1016
- // omit it and let ClawRouter pick (matches the SDK upgrade path).
1017
- routingProfile: routing_profile === "free" ? void 0 : routing_profile,
1025
+ stop,
1026
+ thinking,
1018
1027
  responseFormat,
1019
- stop
1020
- }));
1021
- recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
1022
- return {
1023
- 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]
1024
1062
 
1025
1063
  ${result.response}` }],
1026
- structuredContent: {
1027
- model_used: result.model,
1028
- response: result.response,
1029
- routing: result.routing
1030
- }
1031
- };
1032
- } catch (error) {
1033
- 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
+ }
1034
1073
  }
1035
- }
1036
- if (messages && messages.length > 0) {
1037
- const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
1038
- const fullMessages = [
1039
- ...system ? [{ role: "system", content: system }] : [],
1040
- ...messages,
1041
- { role: "user", content: message }
1042
- ];
1043
- try {
1044
- const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
1045
- maxTokens: max_tokens,
1046
- temperature,
1047
- responseFormat,
1048
- stop
1049
- }));
1050
- const reply = result.choices?.[0]?.message?.content || "";
1051
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1052
- return {
1053
- 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]
1054
1092
 
1055
1093
  ${reply}` }],
1056
- structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
1057
- };
1058
- } catch (error) {
1059
- 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
+ }
1060
1099
  }
1061
- }
1062
- if (model) {
1063
- try {
1064
- const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
1065
- system,
1066
- maxTokens: max_tokens,
1067
- temperature,
1068
- responseFormat,
1069
- stop
1070
- }));
1071
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1072
- return { content: [{ type: "text", text: response }] };
1073
- } catch (error) {
1074
- return {
1075
- content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
1076
- isError: true
1077
- };
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
+ }
1078
1117
  }
1079
- }
1080
- const routingMode = mode || "balanced";
1081
- const models = MODEL_TIERS[routingMode];
1082
- let lastError = null;
1083
- for (const m of models) {
1084
- try {
1085
- const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
1086
- system,
1087
- maxTokens: max_tokens,
1088
- temperature,
1089
- responseFormat,
1090
- stop
1091
- }));
1092
- recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1093
- return {
1094
- 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}]
1095
1133
 
1096
1134
  ${response}` }],
1097
- structuredContent: { model_used: m, response }
1098
- };
1099
- } catch (error) {
1100
- lastError = error;
1101
- continue;
1135
+ structuredContent: { model_used: m, response }
1136
+ };
1137
+ } catch (error) {
1138
+ lastError = error;
1139
+ continue;
1140
+ }
1102
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();
1103
1149
  }
1104
- const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
1105
- return {
1106
- content: [{ type: "text", text: formatError(errorMessage) }],
1107
- isError: true
1108
- };
1109
1150
  }
1110
1151
  );
1111
1152
  }
@@ -1193,6 +1234,10 @@ async function toImageDataUri(ref) {
1193
1234
  if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
1194
1235
  const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
1195
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
+ }
1196
1241
  const buffer2 = Buffer.from(await res.arrayBuffer());
1197
1242
  if (buffer2.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
1198
1243
  throw new Error(`image too large: ${(buffer2.byteLength / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
@@ -1349,34 +1394,42 @@ Source images and masks accept a base64 data URI, an http(s) URL, or a local fil
1349
1394
  };
1350
1395
  }
1351
1396
  const estimatedCost = estimateCost(selectedModel, size);
1352
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1353
- if (!budgetCheck.allowed) {
1397
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1398
+ if (!gate.allowed) {
1354
1399
  return {
1355
- 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.` }],
1356
1401
  isError: true
1357
1402
  };
1358
1403
  }
1359
- response = await getImageClient().edit(prompt, normalizedImage, {
1360
- model: selectedModel,
1361
- size,
1362
- ...normalizedMask ? { mask: normalizedMask } : {}
1363
- });
1364
- 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
+ }
1365
1414
  } else {
1366
1415
  const estimatedCost = estimateCost(selectedModel, size);
1367
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1368
- if (!budgetCheck.allowed) {
1416
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
1417
+ if (!gate.allowed) {
1369
1418
  return {
1370
- 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.` }],
1371
1420
  isError: true
1372
1421
  };
1373
1422
  }
1374
- response = await getImageClient().generate(prompt, {
1375
- model: selectedModel,
1376
- size,
1377
- quality
1378
- });
1379
- 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
+ }
1380
1433
  }
1381
1434
  const imageUrl = response.data?.[0]?.url;
1382
1435
  if (!imageUrl) {
@@ -1417,11 +1470,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
1417
1470
  const controller = new AbortController();
1418
1471
  const id = setTimeout(() => controller.abort(), timeoutMs);
1419
1472
  id.unref?.();
1420
- try {
1421
- return await fetch(url, { ...options, signal: controller.signal });
1422
- } finally {
1423
- clearTimeout(id);
1424
- }
1473
+ return await fetch(url, { ...options, signal: controller.signal });
1425
1474
  }
1426
1475
  function isTimeoutError(err) {
1427
1476
  const name = err instanceof Error ? err.name : "";
@@ -1460,6 +1509,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1460
1509
  }
1461
1510
  },
1462
1511
  async ({ prompt, instrumental, lyrics, model, agent_id }) => {
1512
+ let gate;
1463
1513
  try {
1464
1514
  if (getChain() !== "base") {
1465
1515
  return {
@@ -1473,10 +1523,10 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1473
1523
  isError: true
1474
1524
  };
1475
1525
  }
1476
- const budgetCheck = checkBudget(budget, agent_id, MUSIC_COST);
1477
- if (!budgetCheck.allowed) {
1526
+ gate = reserveBudget(budget, agent_id, MUSIC_COST);
1527
+ if (!gate.allowed) {
1478
1528
  return {
1479
- 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.` }],
1480
1530
  isError: true
1481
1531
  };
1482
1532
  }
@@ -1491,8 +1541,8 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1491
1541
  body: JSON.stringify(body)
1492
1542
  }, 15e3);
1493
1543
  if (resp402.status !== 402) {
1494
- const data2 = await resp402.json();
1495
- 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)}`);
1496
1546
  }
1497
1547
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1498
1548
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -1553,7 +1603,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1553
1603
  };
1554
1604
  } catch (err) {
1555
1605
  const errMsg = err instanceof Error ? err.message : String(err);
1556
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
1606
+ if (isPaymentRejectionError(errMsg)) {
1557
1607
  return {
1558
1608
  content: [{ type: "text", text: `Music generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
1559
1609
  Error: ${errMsg}` }],
@@ -1571,6 +1621,8 @@ Error: ${errMsg}` }],
1571
1621
  content: [{ type: "text", text: formatError(`Music generation failed: ${errMsg}`) }],
1572
1622
  isError: true
1573
1623
  };
1624
+ } finally {
1625
+ gate?.release();
1574
1626
  }
1575
1627
  }
1576
1628
  );
@@ -1640,6 +1692,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1640
1692
  }
1641
1693
  },
1642
1694
  async ({ action, input, voice, model, response_format, speed, duration_seconds, prompt_influence, agent_id }) => {
1695
+ let gate;
1643
1696
  try {
1644
1697
  if (action === "voices") {
1645
1698
  return await listVoices();
@@ -1684,10 +1737,10 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1684
1737
  if (speed !== void 0) body.speed = speed;
1685
1738
  cost = speechCost(model, input.length);
1686
1739
  }
1687
- const budgetCheck = checkBudget(budget, agent_id, cost);
1688
- if (!budgetCheck.allowed) {
1740
+ gate = reserveBudget(budget, agent_id, cost);
1741
+ if (!gate.allowed) {
1689
1742
  return {
1690
- 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.` }],
1691
1744
  isError: true
1692
1745
  };
1693
1746
  }
@@ -1700,7 +1753,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1700
1753
  }, 15e3);
1701
1754
  if (resp402.status !== 402) {
1702
1755
  const data2 = await resp402.json().catch(() => ({}));
1703
- 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)}`);
1704
1757
  }
1705
1758
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1706
1759
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -1766,7 +1819,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1766
1819
  };
1767
1820
  } catch (err) {
1768
1821
  const errMsg = err instanceof Error ? err.message : String(err);
1769
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
1822
+ if (isPaymentRejectionError(errMsg)) {
1770
1823
  return {
1771
1824
  content: [{ type: "text", text: `Speech generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
1772
1825
  Error: ${errMsg}` }],
@@ -1784,6 +1837,8 @@ Error: ${errMsg}` }],
1784
1837
  content: [{ type: "text", text: formatError(`Speech generation failed: ${errMsg}`) }],
1785
1838
  isError: true
1786
1839
  };
1840
+ } finally {
1841
+ gate?.release();
1787
1842
  }
1788
1843
  }
1789
1844
  );
@@ -1884,6 +1939,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1884
1939
  }
1885
1940
  },
1886
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;
1887
1943
  try {
1888
1944
  if (getChain() !== "base") {
1889
1945
  return {
@@ -1924,10 +1980,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1924
1980
  const hasImageInput = Boolean(image_url || real_face_asset_id);
1925
1981
  const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
1926
1982
  const estimatedCost = perSecond * billedSeconds;
1927
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1928
- if (!budgetCheck.allowed) {
1983
+ gate = reserveBudget(budget, agent_id, estimatedCost);
1984
+ if (!gate.allowed) {
1929
1985
  return {
1930
- 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.` }],
1931
1987
  isError: true
1932
1988
  };
1933
1989
  }
@@ -1948,8 +2004,8 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1948
2004
  body: JSON.stringify(body)
1949
2005
  }, 15e3);
1950
2006
  if (resp402.status !== 402) {
1951
- const data = await resp402.json();
1952
- 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)}`);
1953
2009
  }
1954
2010
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
1955
2011
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -2053,7 +2109,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
2053
2109
  };
2054
2110
  } catch (err) {
2055
2111
  const errMsg = err instanceof Error ? err.message : String(err);
2056
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
2112
+ if (isPaymentRejectionError(errMsg)) {
2057
2113
  return {
2058
2114
  content: [{ type: "text", text: `Video generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
2059
2115
  Error: ${errMsg}` }],
@@ -2071,6 +2127,8 @@ Error: ${errMsg}` }],
2071
2127
  content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`, { altModels: "bytedance/seedance-2.0, azure/sora-2" }) }],
2072
2128
  isError: true
2073
2129
  };
2130
+ } finally {
2131
+ gate?.release();
2074
2132
  }
2075
2133
  }
2076
2134
  );
@@ -2096,7 +2154,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
2096
2154
  }, 15e3);
2097
2155
  if (resp402.status !== 402) {
2098
2156
  const data2 = await resp402.json().catch(() => ({}));
2099
- 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)}`);
2100
2158
  }
2101
2159
  const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
2102
2160
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
@@ -2157,6 +2215,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
2157
2215
  }
2158
2216
  },
2159
2217
  async ({ action, name, group_id, image_url, agent_id }) => {
2218
+ let gate;
2160
2219
  try {
2161
2220
  if (action === "init") {
2162
2221
  if (!name) {
@@ -2283,9 +2342,9 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2283
2342
  if (!name || !image_url) {
2284
2343
  return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
2285
2344
  }
2286
- const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2287
- if (!budgetCheck.allowed) {
2288
- 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 };
2289
2348
  }
2290
2349
  const { status, data, settledUsd } = await payAndPostJson(
2291
2350
  `${BLOCKRUN_API4}/v1/portrait/enroll`,
@@ -2333,61 +2392,26 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2333
2392
  if (!name || !image_url || !group_id) {
2334
2393
  return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
2335
2394
  }
2336
- const budgetCheck = checkBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
2337
- if (!budgetCheck.allowed) {
2338
- 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 };
2339
2398
  }
2340
- const privateKey = getOrCreateWalletKey();
2341
- const account = privateKeyToAccount4(privateKey);
2342
- const enrollUrl = `${BLOCKRUN_API4}/v1/realface/enroll`;
2343
- const reqBody = JSON.stringify({ name, image_url, group_id });
2344
- const resp402 = await fetchWithTimeout(enrollUrl, {
2345
- method: "POST",
2346
- headers: { "Content-Type": "application/json" },
2347
- body: reqBody
2348
- }, 15e3);
2349
- if (resp402.status !== 402) {
2350
- const data2 = await resp402.json().catch(() => ({}));
2351
- throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
2352
- }
2353
- const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
2354
- if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
2355
- const paymentRequired = parsePaymentRequired4(prHeader);
2356
- const details = extractPaymentDetails4(paymentRequired);
2357
- const settledUsd = amountToUsd(details.amount);
2358
- const paymentPayload = await createPaymentPayload4(
2359
- privateKey,
2360
- account.address,
2361
- details.recipient,
2362
- details.amount,
2363
- details.network || "eip155:8453",
2364
- {
2365
- resourceUrl: details.resource?.url || enrollUrl,
2366
- resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
2367
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
2368
- extra: details.extra
2369
- }
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"
2370
2403
  );
2371
- const resp = await fetchWithTimeout(enrollUrl, {
2372
- method: "POST",
2373
- headers: {
2374
- "Content-Type": "application/json",
2375
- "PAYMENT-SIGNATURE": paymentPayload
2376
- },
2377
- body: reqBody
2378
- }, 9e4);
2379
- const data = await resp.json().catch(() => ({}));
2380
- if (resp.status === 402) {
2404
+ if (status === 402) {
2381
2405
  throw new Error("Payment rejected. Check your wallet balance.");
2382
2406
  }
2383
- if (resp.status === 425) {
2407
+ if (status === 425) {
2384
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 };
2385
2409
  }
2386
- if (resp.status === 422) {
2410
+ if (status === 422) {
2387
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 };
2388
2412
  }
2389
- if (!resp.ok) {
2390
- 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)}`);
2391
2415
  }
2392
2416
  const assetId = data.asset_id;
2393
2417
  if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
@@ -2416,7 +2440,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2416
2440
  return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
2417
2441
  } catch (err) {
2418
2442
  const errMsg = err instanceof Error ? err.message : String(err);
2419
- if (errMsg.includes("balance") || errMsg.includes("payment") || errMsg.includes("402") || errMsg.includes("rejected")) {
2443
+ if (isPaymentRejectionError(errMsg)) {
2420
2444
  return {
2421
2445
  content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
2422
2446
  Error: ${errMsg}` }],
@@ -2424,6 +2448,8 @@ Error: ${errMsg}` }],
2424
2448
  };
2425
2449
  }
2426
2450
  return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
2451
+ } finally {
2452
+ gate?.release();
2427
2453
  }
2428
2454
  }
2429
2455
  );
@@ -2447,6 +2473,19 @@ function asStructuredContent(result) {
2447
2473
  return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
2448
2474
  }
2449
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
+
2450
2489
  // src/tools/search.ts
2451
2490
  var SEARCH_PRICE_PER_SOURCE = 0.025;
2452
2491
  var SEARCH_DEFAULT_MAX_RESULTS = 10;
@@ -2477,23 +2516,30 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
2477
2516
  async ({ path: path5, body, agent_id }) => {
2478
2517
  try {
2479
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
+ }
2480
2523
  const estimatedCost = estimateSearchCost(body);
2481
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2482
- if (!budgetCheck.allowed) {
2524
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2525
+ if (!gate.allowed) {
2483
2526
  return {
2484
- 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.` }],
2485
2528
  isError: true
2486
2529
  };
2487
2530
  }
2488
- const client = getClient();
2489
- const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
2490
- const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
2491
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2492
- recordSpending(budget, estimatedCost, agent_id);
2493
- return {
2494
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2495
- structuredContent: asStructuredContent(result)
2496
- };
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
+ }
2497
2543
  } catch (err) {
2498
2544
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2499
2545
  }
@@ -2535,23 +2581,30 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
2535
2581
  async ({ path: path5, body, agent_id }) => {
2536
2582
  try {
2537
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
+ }
2538
2588
  const estimatedCost = estimateExaCost(path5, body);
2539
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2540
- if (!budgetCheck.allowed) {
2589
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2590
+ if (!gate.allowed) {
2541
2591
  return {
2542
- 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.` }],
2543
2593
  isError: true
2544
2594
  };
2545
2595
  }
2546
- const client = getClient();
2547
- const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
2548
- const endpoint = `/v1/exa/${cleanPath}`;
2549
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2550
- recordSpending(budget, estimatedCost, agent_id);
2551
- return {
2552
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2553
- structuredContent: asStructuredContent(result)
2554
- };
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
+ }
2555
2608
  } catch (err) {
2556
2609
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2557
2610
  }
@@ -2634,21 +2687,28 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
2634
2687
  async ({ path: path5, params, body, agent_id }) => {
2635
2688
  try {
2636
2689
  body = coerceBody(body);
2690
+ if (hasPathTraversal(path5)) {
2691
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
2692
+ }
2637
2693
  const estimatedCost = estimateMarketCost(path5, body);
2638
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2639
- if (!budgetCheck.allowed) {
2694
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2695
+ if (!gate.allowed) {
2640
2696
  return {
2641
- 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.` }],
2642
2698
  isError: true
2643
2699
  };
2644
2700
  }
2645
- const llm = getClient();
2646
- const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
2647
- recordSpending(budget, estimatedCost, agent_id);
2648
- return {
2649
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2650
- structuredContent: asStructuredContent(result)
2651
- };
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
+ }
2652
2712
  } catch (err) {
2653
2713
  return {
2654
2714
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -2730,51 +2790,55 @@ Examples:
2730
2790
  };
2731
2791
  }
2732
2792
  const estimatedCost = paid ? 1e-3 : 0;
2733
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2734
- if (!budgetCheck.allowed) {
2793
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2794
+ if (!gate.allowed) {
2735
2795
  return {
2736
- 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.` }],
2737
2797
  isError: true
2738
2798
  };
2739
2799
  }
2740
- const priceClient = getPriceClient(paid);
2741
- if (action === "price") {
2742
- if (!symbol) throw new Error("symbol is required for action='price'");
2743
- const result2 = await priceClient.price(category, symbol, {
2744
- market,
2745
- session
2746
- });
2747
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2748
- return {
2749
- content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2750
- structuredContent: result2
2751
- };
2752
- }
2753
- if (action === "history") {
2754
- if (!symbol) throw new Error("symbol is required for action='history'");
2755
- if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
2756
- 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, {
2757
2831
  market,
2758
- session,
2759
- resolution: resolution ?? "D",
2760
- from,
2761
- to
2832
+ query,
2833
+ limit
2762
2834
  });
2763
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2764
2835
  return {
2765
- content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
2766
- structuredContent: result2
2836
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2837
+ structuredContent: result
2767
2838
  };
2839
+ } finally {
2840
+ gate.release();
2768
2841
  }
2769
- const result = await priceClient.listSymbols(category, {
2770
- market,
2771
- query,
2772
- limit
2773
- });
2774
- return {
2775
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2776
- structuredContent: result
2777
- };
2778
2842
  } catch (err) {
2779
2843
  return {
2780
2844
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -2905,22 +2969,29 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
2905
2969
  try {
2906
2970
  body = coerceBody(body);
2907
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
+ }
2908
2975
  const estimatedCost = estimateModalCost(cleanPath);
2909
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2910
- if (!budgetCheck.allowed) {
2976
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
2977
+ if (!gate.allowed) {
2911
2978
  return {
2912
- 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.` }],
2913
2980
  isError: true
2914
2981
  };
2915
2982
  }
2916
- const client = buildClientWithTimeout(modalTimeoutMs(body));
2917
- const endpoint = `/v1/modal/${cleanPath}`;
2918
- const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2919
- recordSpending(budget, estimatedCost, agent_id);
2920
- return {
2921
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2922
- structuredContent: asStructuredContent(result)
2923
- };
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
+ }
2924
2995
  } catch (err) {
2925
2996
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2926
2997
  }
@@ -2971,22 +3042,29 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
2971
3042
  try {
2972
3043
  body = coerceBody(body);
2973
3044
  const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
3045
+ if (hasPathTraversal(cleanPath)) {
3046
+ return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
3047
+ }
2974
3048
  const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
2975
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
2976
- if (!budgetCheck.allowed) {
3049
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3050
+ if (!gate.allowed) {
2977
3051
  return {
2978
- 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.` }],
2979
3053
  isError: true
2980
3054
  };
2981
3055
  }
2982
- const client = getClient();
2983
- const endpoint = `/v1/${cleanPath}`;
2984
- const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
2985
- if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2986
- return {
2987
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2988
- structuredContent: asStructuredContent(result)
2989
- };
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
+ }
2990
3068
  } catch (err) {
2991
3069
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
2992
3070
  }
@@ -3069,22 +3147,29 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
3069
3147
  try {
3070
3148
  body = coerceBody(body);
3071
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
+ }
3072
3153
  const estimatedCost = estimateSurfCost(cleanPath);
3073
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3074
- if (!budgetCheck.allowed) {
3154
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3155
+ if (!gate.allowed) {
3075
3156
  return {
3076
- 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.` }],
3077
3158
  isError: true
3078
3159
  };
3079
3160
  }
3080
- const client = getClient();
3081
- const endpoint = `/v1/surf/${cleanPath}`;
3082
- const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
3083
- recordSpending(budget, estimatedCost, agent_id);
3084
- return {
3085
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3086
- structuredContent: asStructuredContent(result)
3087
- };
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
+ }
3088
3173
  } catch (err) {
3089
3174
  return {
3090
3175
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3138,21 +3223,31 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
3138
3223
  }
3139
3224
  const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
3140
3225
  const estimatedCost = RPC_PRICE_USD * batchCount;
3141
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3142
- if (!budgetCheck.allowed) {
3226
+ const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
3227
+ if (!isValidNetworkSlug(cleanNetwork)) {
3143
3228
  return {
3144
- 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'.`) }],
3145
3230
  isError: true
3146
3231
  };
3147
3232
  }
3148
- const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
3149
- const client = getClient();
3150
- const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
3151
- recordSpending(budget, estimatedCost, agent_id);
3152
- return {
3153
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3154
- structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3155
- };
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
+ }
3156
3251
  } catch (err) {
3157
3252
  return {
3158
3253
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3195,21 +3290,28 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
3195
3290
  async ({ path: path5, agent_id }) => {
3196
3291
  try {
3197
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
+ }
3198
3296
  const estimatedCost = estimateDefiCost(cleanPath);
3199
- const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
3200
- if (!budgetCheck.allowed) {
3297
+ const gate = reserveBudget(budget, agent_id, estimatedCost);
3298
+ if (!gate.allowed) {
3201
3299
  return {
3202
- 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.` }],
3203
3301
  isError: true
3204
3302
  };
3205
3303
  }
3206
- const client = getClient();
3207
- const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
3208
- recordSpending(budget, estimatedCost, agent_id);
3209
- return {
3210
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3211
- structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
3212
- };
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
+ }
3213
3315
  } catch (err) {
3214
3316
  return {
3215
3317
  content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
@@ -3268,7 +3370,7 @@ function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
3268
3370
  }
3269
3371
  function resolveTools(argv, env) {
3270
3372
  const requested = resolveProfileName(argv, env);
3271
- const spec = PROFILES[requested];
3373
+ const spec = Object.hasOwn(PROFILES, requested) ? PROFILES[requested] : void 0;
3272
3374
  if (!spec) {
3273
3375
  return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
3274
3376
  }
@@ -3359,6 +3461,10 @@ function looksLikeRawPrivateKey(value) {
3359
3461
  if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
3360
3462
  return false;
3361
3463
  }
3464
+ function looksLikeNamedSecretValue(value) {
3465
+ if (looksLikeRawPrivateKey(value)) return true;
3466
+ return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
3467
+ }
3362
3468
  function looksLikeSolanaSecretKeyArray(value) {
3363
3469
  return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
3364
3470
  }
@@ -3373,7 +3479,7 @@ function walk(obj, file, jsonPath, out) {
3373
3479
  }
3374
3480
  for (const [k, v] of Object.entries(obj)) {
3375
3481
  const next = jsonPath ? `${jsonPath}.${k}` : k;
3376
- if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeRawPrivateKey(v)) {
3482
+ if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeNamedSecretValue(v)) {
3377
3483
  out.push({ file, path: next });
3378
3484
  } else if (looksLikeRawPrivateKey(v)) {
3379
3485
  out.push({ file, path: next });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "mcpName": "io.github.BlockRunAI/blockrun-mcp",
5
5
  "description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, crypto data, X/Twitter intelligence. Paid via x402 micropayments.",
6
6
  "type": "module",