@blockrun/mcp 0.22.3 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -100,9 +100,10 @@ function getOrCreateWalletKey() {
100
100
  const info = ensureEvmWallet();
101
101
  return info.privateKey;
102
102
  }
103
- function buildSolanaClient() {
103
+ function buildSolanaClient(timeout) {
104
104
  const privateKey = process.env.SOLANA_WALLET_KEY || loadSolanaWallet() || void 0;
105
- return new SolanaLLMClient(privateKey ? { privateKey } : void 0);
105
+ const opts = { ...privateKey ? { privateKey } : {}, ...timeout ? { timeout } : {} };
106
+ return new SolanaLLMClient(Object.keys(opts).length ? opts : void 0);
106
107
  }
107
108
  function getClient() {
108
109
  if (getChain() === "solana") {
@@ -117,6 +118,13 @@ function getClient() {
117
118
  }
118
119
  return _evmClient;
119
120
  }
121
+ function buildClientWithTimeout(timeoutMs) {
122
+ if (getChain() === "solana") {
123
+ return buildSolanaClient(timeoutMs);
124
+ }
125
+ const privateKey = getOrCreateWalletKey();
126
+ return new LLMClient({ privateKey, timeout: timeoutMs });
127
+ }
120
128
  function getAnthropicClient() {
121
129
  if (!_anthropicClient) {
122
130
  const privateKey = getOrCreateWalletKey();
@@ -221,6 +229,58 @@ async function loadModels(llm, cache) {
221
229
  return cache.models;
222
230
  }
223
231
 
232
+ // src/utils/budget.ts
233
+ var EPSILON = 1e-9;
234
+ function formatUsd(amount) {
235
+ return `$${amount.toFixed(amount >= 1 ? 2 : 4)}`;
236
+ }
237
+ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
238
+ const cost = Math.max(0, estimatedCost);
239
+ if (cost > 0 && budget.limit !== null && budget.spent + cost > budget.limit + EPSILON) {
240
+ const remaining = Math.max(0, budget.limit - budget.spent);
241
+ return {
242
+ allowed: false,
243
+ reason: `Global budget limit ${formatUsd(budget.limit)} would be exceeded (${formatUsd(budget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
244
+ };
245
+ }
246
+ if (agentId) {
247
+ const agentBudget = budget.agents.get(agentId);
248
+ if (cost > 0 && agentBudget && agentBudget.spent + cost > agentBudget.limit + EPSILON) {
249
+ const remaining = Math.max(0, agentBudget.limit - agentBudget.spent);
250
+ return {
251
+ allowed: false,
252
+ reason: `Agent "${agentId}" budget ${formatUsd(agentBudget.limit)} would be exceeded (${formatUsd(agentBudget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
253
+ };
254
+ }
255
+ }
256
+ return { allowed: true };
257
+ }
258
+ function recordSpending(budget, cost, agentId) {
259
+ budget.spent += cost;
260
+ budget.calls += 1;
261
+ if (agentId) {
262
+ const agentBudget = budget.agents.get(agentId);
263
+ if (agentBudget) {
264
+ agentBudget.spent += cost;
265
+ agentBudget.calls += 1;
266
+ }
267
+ }
268
+ }
269
+ function amountToUsd(amount) {
270
+ const n = typeof amount === "string" ? Number(amount) : typeof amount === "number" ? amount : NaN;
271
+ if (!Number.isFinite(n) || n <= 0) return null;
272
+ return n / 1e6;
273
+ }
274
+ function recordActualSpend(budget, actualUsd, estimate, agentId) {
275
+ const cost = typeof actualUsd === "number" && Number.isFinite(actualUsd) && actualUsd > 0 ? actualUsd : Math.max(0, estimate);
276
+ recordSpending(budget, cost, agentId);
277
+ }
278
+ function parseBudgetLimitEnv(raw) {
279
+ if (!raw) return null;
280
+ const n = Number(raw.trim().replace(/^\$/, ""));
281
+ return Number.isFinite(n) && n > 0 ? n : null;
282
+ }
283
+
224
284
  // src/tools/wallet.ts
225
285
  import { z } from "zod";
226
286
 
@@ -229,7 +289,6 @@ import QRCode from "qrcode";
229
289
  import open from "open";
230
290
  import * as fs2 from "fs";
231
291
  import * as path3 from "path";
232
- import sharp from "sharp";
233
292
 
234
293
  // src/utils/constants.ts
235
294
  import * as path2 from "path";
@@ -255,6 +314,17 @@ var MODEL_TIERS = {
255
314
 
256
315
  // src/utils/qr.ts
257
316
  var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
317
+ var sharpModule;
318
+ async function loadSharp() {
319
+ if (sharpModule !== void 0) return sharpModule;
320
+ try {
321
+ const mod = await import("sharp");
322
+ sharpModule = mod.default;
323
+ } catch {
324
+ sharpModule = null;
325
+ }
326
+ return sharpModule;
327
+ }
258
328
  function getEip681Uri(address, amountUsdc = 1) {
259
329
  const amountWei = Math.floor(amountUsdc * 1e6);
260
330
  return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
@@ -279,6 +349,8 @@ function buildSolanaLogoSvg(size) {
279
349
  }
280
350
  async function overlayLogo(qrBuf, chain, qrSize) {
281
351
  if (chain !== "solana") return qrBuf;
352
+ const sharp = await loadSharp();
353
+ if (!sharp) return qrBuf;
282
354
  const logoSize = Math.round(qrSize * 0.18);
283
355
  const pad = Math.round(logoSize * 0.08);
284
356
  const logoBuf = await sharp(Buffer.from(buildSolanaLogoSvg(logoSize))).resize(logoSize, logoSize).extend({ top: pad, bottom: pad, left: pad, right: pad, background: { r: 255, g: 255, b: 255, alpha: 1 } }).toBuffer();
@@ -394,8 +466,9 @@ To pay on Solana (no env vars, no file editing, no restart):
394
466
  1. action:"chain" chain:"solana" \u2192 provisions + activates the Solana wallet
395
467
  2. action:"setup" \u2192 Solana address + funding QR (send USDC SPL on Solana)
396
468
  Switch back with action:"chain" chain:"base". Base-only \u2014 these ignore Solana and
397
- need Base: blockrun_image, blockrun_music, blockrun_speech, blockrun_video, paid
398
- blockrun_price, blockrun_chat routing:"smart", and native Anthropic (claude-*).
469
+ need Base: blockrun_image, blockrun_music, blockrun_speech, blockrun_video,
470
+ blockrun_realface, paid blockrun_price, blockrun_chat routing:"smart", and native
471
+ Anthropic (claude-*).
399
472
 
400
473
  Actions:
401
474
  - status (default): Both wallet addresses + USDC balances, active chain, session spending
@@ -671,45 +744,24 @@ Paying on ${chain} | View active: ${info.explorerUrl}${info.isNew ? "\nNEW WALLE
671
744
  import { z as z2 } from "zod";
672
745
  import { LLMClient as LLMClient2 } from "@blockrun/llm";
673
746
 
674
- // src/utils/budget.ts
675
- var EPSILON = 1e-9;
676
- function formatUsd(amount) {
677
- return `$${amount.toFixed(amount >= 1 ? 2 : 4)}`;
678
- }
679
- function checkBudget(budget, agentId, estimatedCost = 1e-3) {
680
- const cost = Math.max(0, estimatedCost);
681
- if (cost > 0 && budget.limit !== null && budget.spent + cost > budget.limit + EPSILON) {
682
- const remaining = Math.max(0, budget.limit - budget.spent);
683
- return {
684
- allowed: false,
685
- reason: `Global budget limit ${formatUsd(budget.limit)} would be exceeded (${formatUsd(budget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
686
- };
687
- }
688
- if (agentId) {
689
- const agentBudget = budget.agents.get(agentId);
690
- if (cost > 0 && agentBudget && agentBudget.spent + cost > agentBudget.limit + EPSILON) {
691
- const remaining = Math.max(0, agentBudget.limit - agentBudget.spent);
692
- return {
693
- allowed: false,
694
- reason: `Agent "${agentId}" budget ${formatUsd(agentBudget.limit)} would be exceeded (${formatUsd(agentBudget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
695
- };
696
- }
697
- }
698
- return { allowed: true };
699
- }
700
- function recordSpending(budget, cost, agentId) {
701
- budget.spent += cost;
702
- budget.calls += 1;
703
- if (agentId) {
704
- const agentBudget = budget.agents.get(agentId);
705
- if (agentBudget) {
706
- agentBudget.spent += cost;
707
- agentBudget.calls += 1;
708
- }
747
+ // src/tools/chat-anthropic.ts
748
+ function anthropicCallCost(model, usage) {
749
+ if (!usage) return null;
750
+ const id = model.toLowerCase();
751
+ let inRate = 5, outRate = 25;
752
+ if (id.includes("opus")) {
753
+ inRate = 15;
754
+ outRate = 75;
755
+ } else if (id.includes("haiku")) {
756
+ inRate = 1;
757
+ outRate = 5;
758
+ } else if (id.includes("sonnet")) {
759
+ inRate = 3;
760
+ outRate = 15;
709
761
  }
762
+ const cost = (usage.input_tokens ?? 0) / 1e6 * inRate + (usage.output_tokens ?? 0) / 1e6 * outRate;
763
+ return cost > 0 ? cost : null;
710
764
  }
711
-
712
- // src/tools/chat-anthropic.ts
713
765
  function isAnthropicModel(model) {
714
766
  const id = model.trim();
715
767
  return /^anthropic\//i.test(id) || /^claude-/i.test(id);
@@ -797,10 +849,9 @@ async function handleAnthropicNative(args) {
797
849
  try {
798
850
  native = await client.messages.create(params);
799
851
  } catch (error) {
800
- const errorMessage = error instanceof Error ? error.message : String(error);
801
- return { content: [{ type: "text", text: formatError(errorMessage) }], isError: true };
852
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
802
853
  }
803
- recordSpending(budget, estimatedCost, agentId);
854
+ recordActualSpend(budget, anthropicCallCost(native.model, native.usage), estimatedCost, agentId);
804
855
  const thinkingBlocks = native.content.filter(isThinkingBlock);
805
856
  const textBlocks = native.content.filter(isTextBlock);
806
857
  const answerText = textBlocks.map((b) => b.text).join("\n");
@@ -838,23 +889,32 @@ ${thinkingText}` });
838
889
  }
839
890
 
840
891
  // src/tools/chat.ts
841
- function estimateChatCost(mode, model, routing, routingProfile) {
892
+ function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
842
893
  if (mode === "free") return 0;
843
894
  if (model?.startsWith("nvidia/")) return 0;
844
895
  if (routing === "smart" && routingProfile === "free") return 0;
896
+ const out = Math.max(maxTokens ?? 1024, 256);
897
+ const frontierReserve = Math.max(0.01, out / 1e6 * 20);
845
898
  if (routing === "smart") {
846
899
  switch (routingProfile) {
847
900
  case "eco":
848
- return 2e-3;
901
+ return 0.01;
849
902
  case "premium":
850
- return 0.05;
903
+ return frontierReserve;
851
904
  case "auto":
852
905
  default:
853
- return 0.01;
906
+ return Math.max(0.01, frontierReserve * 0.5);
854
907
  }
855
908
  }
856
- if (mode === "reasoning" || mode === "powerful") return 0.01;
857
- return 1e-3;
909
+ if (mode === "reasoning" || mode === "powerful") return frontierReserve;
910
+ if (model) return frontierReserve;
911
+ return Math.max(2e-3, out / 1e6 * 3);
912
+ }
913
+ async function withSettledCost(client, run) {
914
+ const before = client.getSpending().totalUsd;
915
+ const result = await run();
916
+ const settledUsd = client.getSpending().totalUsd - before;
917
+ return { result, settledUsd };
858
918
  }
859
919
  function registerChatTool(server, budget) {
860
920
  server.registerTool(
@@ -866,13 +926,13 @@ Notable modes:
866
926
  - mode:"glm" \u2192 Zhipu GLM-5 / GLM-5-Turbo ($0.001/call, excellent for coding tasks, pays via USDC on BlockRun)
867
927
  - mode:"coding" \u2192 GLM-5 first, then code-specialized models
868
928
  - mode:"cheap" \u2192 GLM-5, NVIDIA free, DeepSeek
869
- - mode:"reasoning" \u2192 o3, o1, DeepSeek-R1
929
+ - mode:"reasoning" \u2192 Claude Opus, o3, o1, deepseek-reasoner
870
930
  - mode:"free" \u2192 NVIDIA models (no cost)
871
931
  - routing:"smart" \u2192 auto-select via ClawRouter
872
932
 
873
933
  Pick directly: model:"zai/glm-5", model:"openai/o3", model:"nvidia/deepseek-v4-flash" (free).
874
934
 
875
- Run blockrun_models to see all 41+ models with pricing.`,
935
+ Run blockrun_models to see all available models with pricing.`,
876
936
  inputSchema: {
877
937
  message: z2.string().describe("Your message to the AI"),
878
938
  model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
@@ -904,7 +964,7 @@ Run blockrun_models to see all 41+ models with pricing.`,
904
964
  async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, thinking, agent_id, messages }) => {
905
965
  const llm = getClient();
906
966
  const responseFormat = response_format ? { type: response_format } : void 0;
907
- const estimatedCost = estimateChatCost(mode, model, routing, routing_profile);
967
+ const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
908
968
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
909
969
  if (!budgetCheck.allowed) {
910
970
  return {
@@ -940,7 +1000,7 @@ Run blockrun_models to see all 41+ models with pricing.`,
940
1000
  };
941
1001
  }
942
1002
  try {
943
- const result = await llm.smartChat(message, {
1003
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
944
1004
  system,
945
1005
  maxTokens: max_tokens,
946
1006
  maxOutputTokens: max_tokens,
@@ -951,8 +1011,8 @@ Run blockrun_models to see all 41+ models with pricing.`,
951
1011
  routingProfile: routing_profile === "free" ? void 0 : routing_profile,
952
1012
  responseFormat,
953
1013
  stop
954
- });
955
- recordSpending(budget, result.routing.costEstimate || 1e-3, agent_id);
1014
+ }));
1015
+ recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
956
1016
  return {
957
1017
  content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
958
1018
 
@@ -964,8 +1024,7 @@ ${result.response}` }],
964
1024
  }
965
1025
  };
966
1026
  } catch (error) {
967
- const errorMessage2 = error instanceof Error ? error.message : String(error);
968
- return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
1027
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
969
1028
  }
970
1029
  }
971
1030
  if (messages && messages.length > 0) {
@@ -976,14 +1035,14 @@ ${result.response}` }],
976
1035
  { role: "user", content: message }
977
1036
  ];
978
1037
  try {
979
- const result = await llm.chatCompletion(targetModel, fullMessages, {
1038
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
980
1039
  maxTokens: max_tokens,
981
1040
  temperature,
982
1041
  responseFormat,
983
1042
  stop
984
- });
1043
+ }));
985
1044
  const reply = result.choices?.[0]?.message?.content || "";
986
- recordSpending(budget, estimatedCost, agent_id);
1045
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
987
1046
  return {
988
1047
  content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
989
1048
 
@@ -991,25 +1050,23 @@ ${reply}` }],
991
1050
  structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
992
1051
  };
993
1052
  } catch (error) {
994
- const errorMessage2 = error instanceof Error ? error.message : String(error);
995
- return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
1053
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
996
1054
  }
997
1055
  }
998
1056
  if (model) {
999
1057
  try {
1000
- const response = await llm.chat(model, message, {
1058
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
1001
1059
  system,
1002
1060
  maxTokens: max_tokens,
1003
1061
  temperature,
1004
1062
  responseFormat,
1005
1063
  stop
1006
- });
1007
- recordSpending(budget, estimatedCost, agent_id);
1064
+ }));
1065
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1008
1066
  return { content: [{ type: "text", text: response }] };
1009
1067
  } catch (error) {
1010
- const errorMessage2 = error instanceof Error ? error.message : String(error);
1011
1068
  return {
1012
- content: [{ type: "text", text: formatError(errorMessage2) }],
1069
+ content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
1013
1070
  isError: true
1014
1071
  };
1015
1072
  }
@@ -1019,14 +1076,14 @@ ${reply}` }],
1019
1076
  let lastError = null;
1020
1077
  for (const m of models) {
1021
1078
  try {
1022
- const response = await llm.chat(m, message, {
1079
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
1023
1080
  system,
1024
1081
  maxTokens: max_tokens,
1025
1082
  temperature,
1026
1083
  responseFormat,
1027
1084
  stop
1028
- });
1029
- recordSpending(budget, estimatedCost, agent_id);
1085
+ }));
1086
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1030
1087
  return {
1031
1088
  content: [{ type: "text", text: `[${m}]
1032
1089
 
@@ -1038,7 +1095,7 @@ ${response}` }],
1038
1095
  continue;
1039
1096
  }
1040
1097
  }
1041
- const errorMessage = lastError?.message || "All models failed";
1098
+ const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
1042
1099
  return {
1043
1100
  content: [{ type: "text", text: formatError(errorMessage) }],
1044
1101
  isError: true
@@ -1063,40 +1120,47 @@ function registerModelsTool(server, modelCache) {
1063
1120
  }
1064
1121
  },
1065
1122
  async ({ category, provider }) => {
1066
- let models = await loadModels(getClient(), modelCache);
1067
- if (provider) {
1068
- const p = provider.toLowerCase();
1069
- models = models.filter((m) => m.id.toLowerCase().startsWith(p + "/"));
1070
- }
1071
- if (category && category !== "all") {
1072
- if (category === "image") {
1073
- models = models.filter((m) => getModelType(m) === "image");
1074
- } else if (category === "embedding") {
1075
- models = models.filter((m) => m.id.includes("embed"));
1076
- } else {
1077
- models = models.filter((m) => "categories" in m && m.categories?.includes(category));
1123
+ try {
1124
+ let models = await loadModels(getClient(), modelCache);
1125
+ if (provider) {
1126
+ const p = provider.toLowerCase();
1127
+ models = models.filter((m) => m.id.toLowerCase().startsWith(p + "/"));
1128
+ }
1129
+ if (category && category !== "all") {
1130
+ if (category === "image") {
1131
+ models = models.filter((m) => getModelType(m) === "image");
1132
+ } else if (category === "embedding") {
1133
+ models = models.filter((m) => m.id.includes("embed"));
1134
+ } else {
1135
+ models = models.filter((m) => "categories" in m && m.categories?.includes(category));
1136
+ }
1078
1137
  }
1079
- }
1080
- const lines = models.map((m) => {
1081
- if (getModelType(m) === "image") {
1082
- const image = m;
1083
- const pricing2 = image.pricePerImage ? `$${image.pricePerImage}/image` : "";
1084
- const sizes = image.supportedSizes?.length ? ` | sizes: ${image.supportedSizes.join(", ")}` : "";
1085
- return `- ${image.id}${pricing2 ? ` (${pricing2})` : ""}${sizes} [image]`;
1086
- }
1087
- const llmModel = m;
1088
- const input = llmModel.inputPrice ? `$${llmModel.inputPrice}/M in` : "";
1089
- const output = llmModel.outputPrice ? `$${llmModel.outputPrice}/M out` : "";
1090
- const pricing = [input, output].filter(Boolean).join(", ");
1091
- const ctx = llmModel.contextWindow ? ` | ${Math.round(llmModel.contextWindow / 1e3)}K ctx` : "";
1092
- const cats = llmModel.categories?.length ? ` [${llmModel.categories.join(", ")}]` : "";
1093
- return `- ${llmModel.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
1094
- });
1095
- return {
1096
- content: [{ type: "text", text: `Models (${models.length}):
1138
+ const lines = models.map((m) => {
1139
+ if (getModelType(m) === "image") {
1140
+ const image = m;
1141
+ const pricing2 = image.pricePerImage ? `$${image.pricePerImage}/image` : "";
1142
+ const sizes = image.supportedSizes?.length ? ` | sizes: ${image.supportedSizes.join(", ")}` : "";
1143
+ return `- ${image.id}${pricing2 ? ` (${pricing2})` : ""}${sizes} [image]`;
1144
+ }
1145
+ const llmModel = m;
1146
+ const input = llmModel.inputPrice ? `$${llmModel.inputPrice}/M in` : "";
1147
+ const output = llmModel.outputPrice ? `$${llmModel.outputPrice}/M out` : "";
1148
+ const pricing = [input, output].filter(Boolean).join(", ");
1149
+ const ctx = llmModel.contextWindow ? ` | ${Math.round(llmModel.contextWindow / 1e3)}K ctx` : "";
1150
+ const cats = llmModel.categories?.length ? ` [${llmModel.categories.join(", ")}]` : "";
1151
+ return `- ${llmModel.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
1152
+ });
1153
+ return {
1154
+ content: [{ type: "text", text: `Models (${models.length}):
1097
1155
  ${lines.join("\n")}` }],
1098
- structuredContent: { count: models.length, models }
1099
- };
1156
+ structuredContent: { count: models.length, models }
1157
+ };
1158
+ } catch (err) {
1159
+ return {
1160
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
1161
+ isError: true
1162
+ };
1163
+ }
1100
1164
  }
1101
1165
  );
1102
1166
  }
@@ -1176,9 +1240,14 @@ var IMAGE_MODELS = [
1176
1240
  "xai/grok-imagine-image",
1177
1241
  "xai/grok-imagine-image-pro"
1178
1242
  ];
1243
+ function isLargerThanBase(size) {
1244
+ const m = /^\s*(\d+)\s*[x×]\s*(\d+)\s*$/i.exec(size);
1245
+ if (!m) return false;
1246
+ return Math.max(Number(m[1]), Number(m[2])) > 1024;
1247
+ }
1179
1248
  function estimateCost(model, size) {
1180
1249
  const base = GENERATE_MODEL_COST[model] ?? 0.06;
1181
- if (size !== "1024x1024" && LARGE_SIZE_COST[model]) {
1250
+ if (LARGE_SIZE_COST[model] && isLargerThanBase(size)) {
1182
1251
  return LARGE_SIZE_COST[model];
1183
1252
  }
1184
1253
  return base;
@@ -1455,7 +1524,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1455
1524
  const track = data.data?.[0];
1456
1525
  if (!track?.url) throw new Error("No track URL in response");
1457
1526
  const txHash = resp.headers.get("X-Payment-Receipt") || resp.headers.get("x-payment-receipt");
1458
- recordSpending(budget, MUSIC_COST, agent_id);
1527
+ recordActualSpend(budget, amountToUsd(details.amount), MUSIC_COST, agent_id);
1459
1528
  const lines = [
1460
1529
  `\u{1F3B5} Track ready!`,
1461
1530
  `URL: ${track.url}`,
@@ -1631,6 +1700,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1631
1700
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1632
1701
  const paymentRequired = parsePaymentRequired2(prHeader);
1633
1702
  const details = extractPaymentDetails2(paymentRequired);
1703
+ const billedUsd = amountToUsd(details.amount) ?? cost;
1634
1704
  const paymentPayload = await createPaymentPayload2(
1635
1705
  privateKey,
1636
1706
  account.address,
@@ -1663,7 +1733,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1663
1733
  const clip = data.data?.[0];
1664
1734
  if (!clip?.url) throw new Error("No audio URL in response");
1665
1735
  const txHash = resp.headers.get("X-Payment-Receipt") || resp.headers.get("x-payment-receipt");
1666
- recordSpending(budget, cost, agent_id);
1736
+ recordActualSpend(budget, billedUsd, cost, agent_id);
1667
1737
  const lines = [
1668
1738
  action === "sound_effect" ? `\u{1F50A} Sound effect ready!` : `\u{1F5E3}\uFE0F Speech ready!`,
1669
1739
  `URL: ${clip.url}`,
@@ -1671,7 +1741,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1671
1741
  ...clip.characters !== void 0 ? [`Characters: ${clip.characters}`] : [],
1672
1742
  ...clip.duration_seconds !== void 0 ? [`Duration: ${clip.duration_seconds}s`] : [],
1673
1743
  `Model: ${data.model || (action === "sound_effect" ? "elevenlabs/sound-effects" : model)}`,
1674
- `Cost: $${cost.toFixed(4)}`,
1744
+ `Cost: $${billedUsd.toFixed(4)}`,
1675
1745
  ...txHash ? [`Tx: ${txHash}`] : [],
1676
1746
  ``,
1677
1747
  `Note: This URL may expire \u2014 download it now if you need to keep the file.`
@@ -1684,7 +1754,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1684
1754
  ...clip.characters !== void 0 ? { characters: clip.characters } : {},
1685
1755
  ...clip.duration_seconds !== void 0 ? { duration_seconds: clip.duration_seconds } : {},
1686
1756
  model: data.model || (action === "sound_effect" ? "elevenlabs/sound-effects" : model),
1687
- cost_usd: cost,
1757
+ cost_usd: billedUsd,
1688
1758
  ...txHash ? { txHash } : {}
1689
1759
  }
1690
1760
  };
@@ -1785,8 +1855,8 @@ function registerVideoTool(server, budget) {
1785
1855
  Turns a text prompt (and optional seed image) into a short MP4 clip. The tool submits the job, then polls until the video is ready (typical total wall-time 60-180s; 5 min hard cap). Payment is settled only when upstream returns a finished video \u2014 if the job fails or we give up, you are not charged.
1786
1856
 
1787
1857
  Models (Seedance defaults bumped to 720p + synced audio on the gateway):
1788
- - azure/sora-2 ($0.10/sec, 720p + synced audio, text-to-video) \u2014 OpenAI Sora 2 via Azure AI Foundry. duration_seconds must be 4, 8, or 12 (4s default -> ~$0.42/clip). No image_url / RealFace.
1789
- - xai/grok-imagine-video ($0.05/sec, 8s default -> $0.42/clip) \u2014 stylized, fast
1858
+ - azure/sora-2 ($0.10/sec, 720p + synced audio, text-to-video) \u2014 OpenAI Sora 2 via Azure AI Foundry. duration_seconds must be 4, 8, or 12 (4s default -> ~$0.40/clip). No image_url / RealFace.
1859
+ - xai/grok-imagine-video ($0.05/sec, 8s default -> $0.40/clip) \u2014 stylized, fast
1790
1860
  - bytedance/seedance-1.5-pro (~$0.092/sec, 720p + audio t2v, 5s default up to 10s) \u2014 cheapest Seedance, token-priced upstream
1791
1861
  - bytedance/seedance-2.0-fast (~$0.238/sec text \xB7 ~$0.140/sec image-to-video, 720p + audio, ~60-80s gen) \u2014 sweet-spot price/quality; supports BytePlus RealFace assets
1792
1862
  - bytedance/seedance-2.0 (~$0.298/sec text \xB7 ~$0.183/sec image-to-video, 720p + audio Pro) \u2014 highest quality; supports BytePlus RealFace assets
@@ -1799,11 +1869,15 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1799
1869
  image_url: z7.string().url().optional().describe("Optional seed image URL for image-to-video generation"),
1800
1870
  real_face_asset_id: z7.string().regex(/^ta_[A-Za-z0-9]+$/, "token360 asset id like 'ta_xxxx'").optional().describe("BytePlus RealFace asset id (from blockrun_realface enroll/list) to generate video of a specific real person. Seedance 2.0 / 2.0-fast only. Mutually exclusive with image_url."),
1801
1871
  duration_seconds: z7.number().int().min(1).max(60).optional().describe("Duration to bill for (defaults to the model's default \u2014 8s for xAI, 5s for Seedance; Seedance supports up to 10s)."),
1872
+ generate_audio: z7.boolean().optional().describe("Seedance only: whether to generate a synced audio track. Defaults ON for text-to-video and OFF for image/RealFace-conditioned. The auto-generated audio is occasionally rejected by upstream moderation ('output audio may contain sensitive information') even for benign prompts \u2014 pass false to skip audio and avoid that failure. Ignored by xAI/Sora."),
1873
+ resolution: z7.enum(["360p", "480p", "540p", "720p", "1080p", "1K", "2K", "4K"]).optional().describe("Seedance only: output resolution. Defaults to 720p. Higher resolutions cost more (token-priced upstream) \u2014 the final price is set by the 402 challenge, so the up-front estimate may understate 1080p/4K. Ignored by xAI/Sora."),
1874
+ aspect_ratio: z7.enum(["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"]).optional().describe("Seedance only: output aspect ratio, e.g. '9:16' for vertical/mobile, '16:9' for landscape. Defaults to the model's own default. Ignored by xAI/Sora."),
1875
+ last_frame_url: z7.string().url().optional().describe("Seedance only: first-and-last-frame interpolation. A second image URL that seeds the FINAL frame so the model tweens from image_url (first frame) \u2192 last_frame_url (last frame). Requires image_url; mutually exclusive with real_face_asset_id. Priced as image-to-video."),
1802
1876
  model: z7.enum(["azure/sora-2", "xai/grok-imagine-video", "bytedance/seedance-1.5-pro", "bytedance/seedance-2.0-fast", "bytedance/seedance-2.0"]).optional().default("xai/grok-imagine-video").describe("Video model to use"),
1803
1877
  agent_id: z7.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1804
1878
  }
1805
1879
  },
1806
- async ({ prompt, image_url, real_face_asset_id, duration_seconds, model, agent_id }) => {
1880
+ async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
1807
1881
  try {
1808
1882
  if (getChain() !== "base") {
1809
1883
  return {
@@ -1826,6 +1900,20 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1826
1900
  };
1827
1901
  }
1828
1902
  }
1903
+ if (last_frame_url) {
1904
+ if (!image_url) {
1905
+ return {
1906
+ content: [{ type: "text", text: formatError("last_frame_url (first-and-last-frame interpolation) requires image_url as the first frame.") }],
1907
+ isError: true
1908
+ };
1909
+ }
1910
+ if (real_face_asset_id) {
1911
+ return {
1912
+ content: [{ type: "text", text: formatError("last_frame_url cannot be combined with real_face_asset_id.") }],
1913
+ isError: true
1914
+ };
1915
+ }
1916
+ }
1829
1917
  const billedSeconds = duration_seconds ?? VIDEO_DEFAULT_DURATION[selectedModel] ?? 8;
1830
1918
  const hasImageInput = Boolean(image_url || real_face_asset_id);
1831
1919
  const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
@@ -1844,6 +1932,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1844
1932
  if (image_url) body.image_url = image_url;
1845
1933
  if (real_face_asset_id) body.real_face_asset_id = real_face_asset_id;
1846
1934
  if (duration_seconds !== void 0) body.duration_seconds = duration_seconds;
1935
+ if (generate_audio !== void 0) body.generate_audio = generate_audio;
1936
+ if (resolution !== void 0) body.resolution = resolution;
1937
+ if (aspect_ratio !== void 0) body.aspect_ratio = aspect_ratio;
1938
+ if (last_frame_url) body.last_frame_url = last_frame_url;
1847
1939
  const resp402 = await fetchWithTimeout(submitUrl, {
1848
1940
  method: "POST",
1849
1941
  headers: { "Content-Type": "application/json" },
@@ -1857,6 +1949,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1857
1949
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1858
1950
  const paymentRequired = parsePaymentRequired3(prHeader);
1859
1951
  const details = extractPaymentDetails3(paymentRequired);
1952
+ const settledUsd = amountToUsd(details.amount);
1860
1953
  const paymentPayload = await createPaymentPayload3(
1861
1954
  privateKey,
1862
1955
  account.address,
@@ -1939,7 +2032,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1939
2032
  ...completed.request_id ? [`Request ID: ${completed.request_id}`] : [],
1940
2033
  ...completed.txHash ? [`Tx: ${completed.txHash}`] : []
1941
2034
  ];
1942
- recordSpending(budget, estimatedCost, agent_id);
2035
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1943
2036
  return {
1944
2037
  content: [{ type: "text", text: lines.join("\n") }],
1945
2038
  structuredContent: {
@@ -2025,7 +2118,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
2025
2118
  body: reqBody
2026
2119
  }, 9e4);
2027
2120
  const data = await resp.json().catch(() => ({}));
2028
- return { status: resp.status, data };
2121
+ return { status: resp.status, data, settledUsd: amountToUsd(details.amount) };
2029
2122
  }
2030
2123
  function registerRealfaceTool(server, budget) {
2031
2124
  server.registerTool(
@@ -2188,7 +2281,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2188
2281
  if (!budgetCheck.allowed) {
2189
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 };
2190
2283
  }
2191
- const { status, data } = await payAndPostJson(
2284
+ const { status, data, settledUsd } = await payAndPostJson(
2192
2285
  `${BLOCKRUN_API4}/v1/portrait/enroll`,
2193
2286
  JSON.stringify({ name, image_url }),
2194
2287
  "BlockRun Virtual Portrait enrollment"
@@ -2204,7 +2297,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2204
2297
  }
2205
2298
  const assetId = data.asset_id;
2206
2299
  if (!assetId) throw new Error(`Portrait response missing asset_id: ${JSON.stringify(data)}`);
2207
- recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2300
+ recordActualSpend(budget, settledUsd, ENROLLMENT_PRICE_USD, agent_id);
2208
2301
  const txHash = data.settlement?.tx_hash || void 0;
2209
2302
  const lines = [
2210
2303
  `\u2705 Virtual Portrait enrolled!`,
@@ -2255,6 +2348,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2255
2348
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
2256
2349
  const paymentRequired = parsePaymentRequired4(prHeader);
2257
2350
  const details = extractPaymentDetails4(paymentRequired);
2351
+ const settledUsd = amountToUsd(details.amount);
2258
2352
  const paymentPayload = await createPaymentPayload4(
2259
2353
  privateKey,
2260
2354
  account.address,
@@ -2291,7 +2385,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2291
2385
  }
2292
2386
  const assetId = data.asset_id;
2293
2387
  if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
2294
- recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2388
+ recordActualSpend(budget, settledUsd, ENROLLMENT_PRICE_USD, agent_id);
2295
2389
  const txHash = data.settlement?.tx_hash || void 0;
2296
2390
  const lines = [
2297
2391
  `\u2705 RealFace enrolled!`,
@@ -2336,13 +2430,16 @@ import { z as z9 } from "zod";
2336
2430
  function coerceBody(body) {
2337
2431
  if (typeof body !== "string") return body;
2338
2432
  const trimmed = body.trim();
2339
- if (trimmed === "") return {};
2433
+ if (trimmed === "") return void 0;
2340
2434
  try {
2341
2435
  return JSON.parse(trimmed);
2342
2436
  } catch {
2343
2437
  return body;
2344
2438
  }
2345
2439
  }
2440
+ function asStructuredContent(result) {
2441
+ return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
2442
+ }
2346
2443
 
2347
2444
  // src/tools/search.ts
2348
2445
  var SEARCH_PRICE_PER_SOURCE = 0.025;
@@ -2389,7 +2486,7 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
2389
2486
  recordSpending(budget, estimatedCost, agent_id);
2390
2487
  return {
2391
2488
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2392
- structuredContent: result
2489
+ structuredContent: asStructuredContent(result)
2393
2490
  };
2394
2491
  } catch (err) {
2395
2492
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2447,7 +2544,7 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
2447
2544
  recordSpending(budget, estimatedCost, agent_id);
2448
2545
  return {
2449
2546
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2450
- structuredContent: result
2547
+ structuredContent: asStructuredContent(result)
2451
2548
  };
2452
2549
  } catch (err) {
2453
2550
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2544,12 +2641,11 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
2544
2641
  recordSpending(budget, estimatedCost, agent_id);
2545
2642
  return {
2546
2643
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2547
- structuredContent: result
2644
+ structuredContent: asStructuredContent(result)
2548
2645
  };
2549
2646
  } catch (err) {
2550
- const errMsg = err instanceof Error ? err.message : String(err);
2551
2647
  return {
2552
- content: [{ type: "text", text: formatError(errMsg) }],
2648
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
2553
2649
  isError: true
2554
2650
  };
2555
2651
  }
@@ -2674,9 +2770,8 @@ Examples:
2674
2770
  structuredContent: result
2675
2771
  };
2676
2772
  } catch (err) {
2677
- const msg = err instanceof Error ? err.message : String(err);
2678
2773
  return {
2679
- content: [{ type: "text", text: formatError(msg) }],
2774
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
2680
2775
  isError: true
2681
2776
  };
2682
2777
  }
@@ -2770,6 +2865,15 @@ import { z as z14 } from "zod";
2770
2865
  function estimateModalCost(path5) {
2771
2866
  return path5.includes("sandbox/create") ? 0.01 : 1e-3;
2772
2867
  }
2868
+ var MODAL_DEFAULT_TIMEOUT_S = 300;
2869
+ var MODAL_MAX_TIMEOUT_S = 1800;
2870
+ var MODAL_SLACK_MS = 15e3;
2871
+ function modalTimeoutMs(body) {
2872
+ const raw = body && typeof body === "object" ? body.timeout : void 0;
2873
+ const requested = typeof raw === "number" && raw > 0 ? raw : MODAL_DEFAULT_TIMEOUT_S;
2874
+ const clamped = Math.min(Math.max(requested, MODAL_DEFAULT_TIMEOUT_S), MODAL_MAX_TIMEOUT_S);
2875
+ return clamped * 1e3 + MODAL_SLACK_MS;
2876
+ }
2773
2877
  function registerModalTool(server, budget) {
2774
2878
  server.registerTool(
2775
2879
  "blockrun_modal",
@@ -2803,13 +2907,13 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
2803
2907
  isError: true
2804
2908
  };
2805
2909
  }
2806
- const client = getClient();
2910
+ const client = buildClientWithTimeout(modalTimeoutMs(body));
2807
2911
  const endpoint = `/v1/modal/${cleanPath}`;
2808
2912
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
2809
2913
  recordSpending(budget, estimatedCost, agent_id);
2810
2914
  return {
2811
2915
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2812
- structuredContent: result
2916
+ structuredContent: asStructuredContent(result)
2813
2917
  };
2814
2918
  } catch (err) {
2815
2919
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2875,7 +2979,7 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
2875
2979
  if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2876
2980
  return {
2877
2981
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2878
- structuredContent: result
2982
+ structuredContent: asStructuredContent(result)
2879
2983
  };
2880
2984
  } catch (err) {
2881
2985
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2973,7 +3077,7 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
2973
3077
  recordSpending(budget, estimatedCost, agent_id);
2974
3078
  return {
2975
3079
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2976
- structuredContent: result
3080
+ structuredContent: asStructuredContent(result)
2977
3081
  };
2978
3082
  } catch (err) {
2979
3083
  return {
@@ -3170,7 +3274,13 @@ function resolveTools(argv, env) {
3170
3274
 
3171
3275
  // src/mcp-handler.ts
3172
3276
  function initializeMcpServer(server, profileArgs) {
3173
- const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
3277
+ const env = profileArgs?.env ?? process.env;
3278
+ const budget = {
3279
+ limit: parseBudgetLimitEnv(env.BLOCKRUN_BUDGET_LIMIT),
3280
+ spent: 0,
3281
+ calls: 0,
3282
+ agents: /* @__PURE__ */ new Map()
3283
+ };
3174
3284
  const modelCache = { models: null };
3175
3285
  const { profile, tools } = resolveTools(profileArgs?.argv, profileArgs?.env);
3176
3286
  const registrars = {
@@ -3243,9 +3353,15 @@ function looksLikeRawPrivateKey(value) {
3243
3353
  if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
3244
3354
  return false;
3245
3355
  }
3356
+ function looksLikeSolanaSecretKeyArray(value) {
3357
+ return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
3358
+ }
3246
3359
  function walk(obj, file, jsonPath, out) {
3247
3360
  if (obj === null || typeof obj !== "object") return;
3248
3361
  if (Array.isArray(obj)) {
3362
+ if (looksLikeSolanaSecretKeyArray(obj)) {
3363
+ out.push({ file, path: jsonPath || "(root)" });
3364
+ }
3249
3365
  obj.forEach((v, i) => walk(v, file, `${jsonPath}[${i}]`, out));
3250
3366
  return;
3251
3367
  }