@blockrun/mcp 0.22.2 → 0.23.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.
package/dist/index.js CHANGED
@@ -221,6 +221,58 @@ async function loadModels(llm, cache) {
221
221
  return cache.models;
222
222
  }
223
223
 
224
+ // src/utils/budget.ts
225
+ var EPSILON = 1e-9;
226
+ function formatUsd(amount) {
227
+ return `$${amount.toFixed(amount >= 1 ? 2 : 4)}`;
228
+ }
229
+ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
230
+ const cost = Math.max(0, estimatedCost);
231
+ if (cost > 0 && budget.limit !== null && budget.spent + cost > budget.limit + EPSILON) {
232
+ const remaining = Math.max(0, budget.limit - budget.spent);
233
+ return {
234
+ allowed: false,
235
+ reason: `Global budget limit ${formatUsd(budget.limit)} would be exceeded (${formatUsd(budget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
236
+ };
237
+ }
238
+ if (agentId) {
239
+ const agentBudget = budget.agents.get(agentId);
240
+ if (cost > 0 && agentBudget && agentBudget.spent + cost > agentBudget.limit + EPSILON) {
241
+ const remaining = Math.max(0, agentBudget.limit - agentBudget.spent);
242
+ return {
243
+ allowed: false,
244
+ reason: `Agent "${agentId}" budget ${formatUsd(agentBudget.limit)} would be exceeded (${formatUsd(agentBudget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
245
+ };
246
+ }
247
+ }
248
+ return { allowed: true };
249
+ }
250
+ function recordSpending(budget, cost, agentId) {
251
+ budget.spent += cost;
252
+ budget.calls += 1;
253
+ if (agentId) {
254
+ const agentBudget = budget.agents.get(agentId);
255
+ if (agentBudget) {
256
+ agentBudget.spent += cost;
257
+ agentBudget.calls += 1;
258
+ }
259
+ }
260
+ }
261
+ function amountToUsd(amount) {
262
+ const n = typeof amount === "string" ? Number(amount) : typeof amount === "number" ? amount : NaN;
263
+ if (!Number.isFinite(n) || n <= 0) return null;
264
+ return n / 1e6;
265
+ }
266
+ function recordActualSpend(budget, actualUsd, estimate, agentId) {
267
+ const cost = typeof actualUsd === "number" && Number.isFinite(actualUsd) && actualUsd > 0 ? actualUsd : Math.max(0, estimate);
268
+ recordSpending(budget, cost, agentId);
269
+ }
270
+ function parseBudgetLimitEnv(raw) {
271
+ if (!raw) return null;
272
+ const n = Number(raw.trim().replace(/^\$/, ""));
273
+ return Number.isFinite(n) && n > 0 ? n : null;
274
+ }
275
+
224
276
  // src/tools/wallet.ts
225
277
  import { z } from "zod";
226
278
 
@@ -229,7 +281,6 @@ import QRCode from "qrcode";
229
281
  import open from "open";
230
282
  import * as fs2 from "fs";
231
283
  import * as path3 from "path";
232
- import sharp from "sharp";
233
284
 
234
285
  // src/utils/constants.ts
235
286
  import * as path2 from "path";
@@ -255,6 +306,17 @@ var MODEL_TIERS = {
255
306
 
256
307
  // src/utils/qr.ts
257
308
  var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
309
+ var sharpModule;
310
+ async function loadSharp() {
311
+ if (sharpModule !== void 0) return sharpModule;
312
+ try {
313
+ const mod = await import("sharp");
314
+ sharpModule = mod.default;
315
+ } catch {
316
+ sharpModule = null;
317
+ }
318
+ return sharpModule;
319
+ }
258
320
  function getEip681Uri(address, amountUsdc = 1) {
259
321
  const amountWei = Math.floor(amountUsdc * 1e6);
260
322
  return `ethereum:${USDC_ADDRESS}@${BASE_CHAIN_ID}/transfer?address=${address}&uint256=${amountWei}`;
@@ -279,6 +341,8 @@ function buildSolanaLogoSvg(size) {
279
341
  }
280
342
  async function overlayLogo(qrBuf, chain, qrSize) {
281
343
  if (chain !== "solana") return qrBuf;
344
+ const sharp = await loadSharp();
345
+ if (!sharp) return qrBuf;
282
346
  const logoSize = Math.round(qrSize * 0.18);
283
347
  const pad = Math.round(logoSize * 0.08);
284
348
  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 +458,9 @@ To pay on Solana (no env vars, no file editing, no restart):
394
458
  1. action:"chain" chain:"solana" \u2192 provisions + activates the Solana wallet
395
459
  2. action:"setup" \u2192 Solana address + funding QR (send USDC SPL on Solana)
396
460
  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-*).
461
+ need Base: blockrun_image, blockrun_music, blockrun_speech, blockrun_video,
462
+ blockrun_realface, paid blockrun_price, blockrun_chat routing:"smart", and native
463
+ Anthropic (claude-*).
399
464
 
400
465
  Actions:
401
466
  - status (default): Both wallet addresses + USDC balances, active chain, session spending
@@ -671,45 +736,24 @@ Paying on ${chain} | View active: ${info.explorerUrl}${info.isNew ? "\nNEW WALLE
671
736
  import { z as z2 } from "zod";
672
737
  import { LLMClient as LLMClient2 } from "@blockrun/llm";
673
738
 
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
- }
739
+ // src/tools/chat-anthropic.ts
740
+ function anthropicCallCost(model, usage) {
741
+ if (!usage) return null;
742
+ const id = model.toLowerCase();
743
+ let inRate = 5, outRate = 25;
744
+ if (id.includes("opus")) {
745
+ inRate = 15;
746
+ outRate = 75;
747
+ } else if (id.includes("haiku")) {
748
+ inRate = 1;
749
+ outRate = 5;
750
+ } else if (id.includes("sonnet")) {
751
+ inRate = 3;
752
+ outRate = 15;
709
753
  }
754
+ const cost = (usage.input_tokens ?? 0) / 1e6 * inRate + (usage.output_tokens ?? 0) / 1e6 * outRate;
755
+ return cost > 0 ? cost : null;
710
756
  }
711
-
712
- // src/tools/chat-anthropic.ts
713
757
  function isAnthropicModel(model) {
714
758
  const id = model.trim();
715
759
  return /^anthropic\//i.test(id) || /^claude-/i.test(id);
@@ -797,10 +841,9 @@ async function handleAnthropicNative(args) {
797
841
  try {
798
842
  native = await client.messages.create(params);
799
843
  } catch (error) {
800
- const errorMessage = error instanceof Error ? error.message : String(error);
801
- return { content: [{ type: "text", text: formatError(errorMessage) }], isError: true };
844
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
802
845
  }
803
- recordSpending(budget, estimatedCost, agentId);
846
+ recordActualSpend(budget, anthropicCallCost(native.model, native.usage), estimatedCost, agentId);
804
847
  const thinkingBlocks = native.content.filter(isThinkingBlock);
805
848
  const textBlocks = native.content.filter(isTextBlock);
806
849
  const answerText = textBlocks.map((b) => b.text).join("\n");
@@ -838,23 +881,32 @@ ${thinkingText}` });
838
881
  }
839
882
 
840
883
  // src/tools/chat.ts
841
- function estimateChatCost(mode, model, routing, routingProfile) {
884
+ function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
842
885
  if (mode === "free") return 0;
843
886
  if (model?.startsWith("nvidia/")) return 0;
844
887
  if (routing === "smart" && routingProfile === "free") return 0;
888
+ const out = Math.max(maxTokens ?? 1024, 256);
889
+ const frontierReserve = Math.max(0.01, out / 1e6 * 20);
845
890
  if (routing === "smart") {
846
891
  switch (routingProfile) {
847
892
  case "eco":
848
- return 2e-3;
893
+ return 0.01;
849
894
  case "premium":
850
- return 0.05;
895
+ return frontierReserve;
851
896
  case "auto":
852
897
  default:
853
- return 0.01;
898
+ return Math.max(0.01, frontierReserve * 0.5);
854
899
  }
855
900
  }
856
- if (mode === "reasoning" || mode === "powerful") return 0.01;
857
- return 1e-3;
901
+ if (mode === "reasoning" || mode === "powerful") return frontierReserve;
902
+ if (model) return frontierReserve;
903
+ return Math.max(2e-3, out / 1e6 * 3);
904
+ }
905
+ async function withSettledCost(client, run) {
906
+ const before = client.getSpending().totalUsd;
907
+ const result = await run();
908
+ const settledUsd = client.getSpending().totalUsd - before;
909
+ return { result, settledUsd };
858
910
  }
859
911
  function registerChatTool(server, budget) {
860
912
  server.registerTool(
@@ -866,13 +918,13 @@ Notable modes:
866
918
  - mode:"glm" \u2192 Zhipu GLM-5 / GLM-5-Turbo ($0.001/call, excellent for coding tasks, pays via USDC on BlockRun)
867
919
  - mode:"coding" \u2192 GLM-5 first, then code-specialized models
868
920
  - mode:"cheap" \u2192 GLM-5, NVIDIA free, DeepSeek
869
- - mode:"reasoning" \u2192 o3, o1, DeepSeek-R1
921
+ - mode:"reasoning" \u2192 Claude Opus, o3, o1, deepseek-reasoner
870
922
  - mode:"free" \u2192 NVIDIA models (no cost)
871
923
  - routing:"smart" \u2192 auto-select via ClawRouter
872
924
 
873
925
  Pick directly: model:"zai/glm-5", model:"openai/o3", model:"nvidia/deepseek-v4-flash" (free).
874
926
 
875
- Run blockrun_models to see all 41+ models with pricing.`,
927
+ Run blockrun_models to see all available models with pricing.`,
876
928
  inputSchema: {
877
929
  message: z2.string().describe("Your message to the AI"),
878
930
  model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
@@ -904,7 +956,7 @@ Run blockrun_models to see all 41+ models with pricing.`,
904
956
  async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, thinking, agent_id, messages }) => {
905
957
  const llm = getClient();
906
958
  const responseFormat = response_format ? { type: response_format } : void 0;
907
- const estimatedCost = estimateChatCost(mode, model, routing, routing_profile);
959
+ const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
908
960
  const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
909
961
  if (!budgetCheck.allowed) {
910
962
  return {
@@ -940,7 +992,7 @@ Run blockrun_models to see all 41+ models with pricing.`,
940
992
  };
941
993
  }
942
994
  try {
943
- const result = await llm.smartChat(message, {
995
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
944
996
  system,
945
997
  maxTokens: max_tokens,
946
998
  maxOutputTokens: max_tokens,
@@ -951,8 +1003,8 @@ Run blockrun_models to see all 41+ models with pricing.`,
951
1003
  routingProfile: routing_profile === "free" ? void 0 : routing_profile,
952
1004
  responseFormat,
953
1005
  stop
954
- });
955
- recordSpending(budget, result.routing.costEstimate || 1e-3, agent_id);
1006
+ }));
1007
+ recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
956
1008
  return {
957
1009
  content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
958
1010
 
@@ -964,8 +1016,7 @@ ${result.response}` }],
964
1016
  }
965
1017
  };
966
1018
  } catch (error) {
967
- const errorMessage2 = error instanceof Error ? error.message : String(error);
968
- return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
1019
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
969
1020
  }
970
1021
  }
971
1022
  if (messages && messages.length > 0) {
@@ -976,14 +1027,14 @@ ${result.response}` }],
976
1027
  { role: "user", content: message }
977
1028
  ];
978
1029
  try {
979
- const result = await llm.chatCompletion(targetModel, fullMessages, {
1030
+ const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
980
1031
  maxTokens: max_tokens,
981
1032
  temperature,
982
1033
  responseFormat,
983
1034
  stop
984
- });
1035
+ }));
985
1036
  const reply = result.choices?.[0]?.message?.content || "";
986
- recordSpending(budget, estimatedCost, agent_id);
1037
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
987
1038
  return {
988
1039
  content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
989
1040
 
@@ -991,25 +1042,23 @@ ${reply}` }],
991
1042
  structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
992
1043
  };
993
1044
  } catch (error) {
994
- const errorMessage2 = error instanceof Error ? error.message : String(error);
995
- return { content: [{ type: "text", text: formatError(errorMessage2) }], isError: true };
1045
+ return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
996
1046
  }
997
1047
  }
998
1048
  if (model) {
999
1049
  try {
1000
- const response = await llm.chat(model, message, {
1050
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
1001
1051
  system,
1002
1052
  maxTokens: max_tokens,
1003
1053
  temperature,
1004
1054
  responseFormat,
1005
1055
  stop
1006
- });
1007
- recordSpending(budget, estimatedCost, agent_id);
1056
+ }));
1057
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1008
1058
  return { content: [{ type: "text", text: response }] };
1009
1059
  } catch (error) {
1010
- const errorMessage2 = error instanceof Error ? error.message : String(error);
1011
1060
  return {
1012
- content: [{ type: "text", text: formatError(errorMessage2) }],
1061
+ content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
1013
1062
  isError: true
1014
1063
  };
1015
1064
  }
@@ -1019,14 +1068,14 @@ ${reply}` }],
1019
1068
  let lastError = null;
1020
1069
  for (const m of models) {
1021
1070
  try {
1022
- const response = await llm.chat(m, message, {
1071
+ const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
1023
1072
  system,
1024
1073
  maxTokens: max_tokens,
1025
1074
  temperature,
1026
1075
  responseFormat,
1027
1076
  stop
1028
- });
1029
- recordSpending(budget, estimatedCost, agent_id);
1077
+ }));
1078
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1030
1079
  return {
1031
1080
  content: [{ type: "text", text: `[${m}]
1032
1081
 
@@ -1038,7 +1087,7 @@ ${response}` }],
1038
1087
  continue;
1039
1088
  }
1040
1089
  }
1041
- const errorMessage = lastError?.message || "All models failed";
1090
+ const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
1042
1091
  return {
1043
1092
  content: [{ type: "text", text: formatError(errorMessage) }],
1044
1093
  isError: true
@@ -1063,40 +1112,47 @@ function registerModelsTool(server, modelCache) {
1063
1112
  }
1064
1113
  },
1065
1114
  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));
1115
+ try {
1116
+ let models = await loadModels(getClient(), modelCache);
1117
+ if (provider) {
1118
+ const p = provider.toLowerCase();
1119
+ models = models.filter((m) => m.id.toLowerCase().startsWith(p + "/"));
1120
+ }
1121
+ if (category && category !== "all") {
1122
+ if (category === "image") {
1123
+ models = models.filter((m) => getModelType(m) === "image");
1124
+ } else if (category === "embedding") {
1125
+ models = models.filter((m) => m.id.includes("embed"));
1126
+ } else {
1127
+ models = models.filter((m) => "categories" in m && m.categories?.includes(category));
1128
+ }
1078
1129
  }
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}):
1130
+ const lines = models.map((m) => {
1131
+ if (getModelType(m) === "image") {
1132
+ const image = m;
1133
+ const pricing2 = image.pricePerImage ? `$${image.pricePerImage}/image` : "";
1134
+ const sizes = image.supportedSizes?.length ? ` | sizes: ${image.supportedSizes.join(", ")}` : "";
1135
+ return `- ${image.id}${pricing2 ? ` (${pricing2})` : ""}${sizes} [image]`;
1136
+ }
1137
+ const llmModel = m;
1138
+ const input = llmModel.inputPrice ? `$${llmModel.inputPrice}/M in` : "";
1139
+ const output = llmModel.outputPrice ? `$${llmModel.outputPrice}/M out` : "";
1140
+ const pricing = [input, output].filter(Boolean).join(", ");
1141
+ const ctx = llmModel.contextWindow ? ` | ${Math.round(llmModel.contextWindow / 1e3)}K ctx` : "";
1142
+ const cats = llmModel.categories?.length ? ` [${llmModel.categories.join(", ")}]` : "";
1143
+ return `- ${llmModel.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
1144
+ });
1145
+ return {
1146
+ content: [{ type: "text", text: `Models (${models.length}):
1097
1147
  ${lines.join("\n")}` }],
1098
- structuredContent: { count: models.length, models }
1099
- };
1148
+ structuredContent: { count: models.length, models }
1149
+ };
1150
+ } catch (err) {
1151
+ return {
1152
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
1153
+ isError: true
1154
+ };
1155
+ }
1100
1156
  }
1101
1157
  );
1102
1158
  }
@@ -1176,9 +1232,14 @@ var IMAGE_MODELS = [
1176
1232
  "xai/grok-imagine-image",
1177
1233
  "xai/grok-imagine-image-pro"
1178
1234
  ];
1235
+ function isLargerThanBase(size) {
1236
+ const m = /^\s*(\d+)\s*[x×]\s*(\d+)\s*$/i.exec(size);
1237
+ if (!m) return false;
1238
+ return Math.max(Number(m[1]), Number(m[2])) > 1024;
1239
+ }
1179
1240
  function estimateCost(model, size) {
1180
1241
  const base = GENERATE_MODEL_COST[model] ?? 0.06;
1181
- if (size !== "1024x1024" && LARGE_SIZE_COST[model]) {
1242
+ if (LARGE_SIZE_COST[model] && isLargerThanBase(size)) {
1182
1243
  return LARGE_SIZE_COST[model];
1183
1244
  }
1184
1245
  return base;
@@ -1455,7 +1516,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
1455
1516
  const track = data.data?.[0];
1456
1517
  if (!track?.url) throw new Error("No track URL in response");
1457
1518
  const txHash = resp.headers.get("X-Payment-Receipt") || resp.headers.get("x-payment-receipt");
1458
- recordSpending(budget, MUSIC_COST, agent_id);
1519
+ recordActualSpend(budget, amountToUsd(details.amount), MUSIC_COST, agent_id);
1459
1520
  const lines = [
1460
1521
  `\u{1F3B5} Track ready!`,
1461
1522
  `URL: ${track.url}`,
@@ -1631,6 +1692,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1631
1692
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1632
1693
  const paymentRequired = parsePaymentRequired2(prHeader);
1633
1694
  const details = extractPaymentDetails2(paymentRequired);
1695
+ const billedUsd = amountToUsd(details.amount) ?? cost;
1634
1696
  const paymentPayload = await createPaymentPayload2(
1635
1697
  privateKey,
1636
1698
  account.address,
@@ -1663,7 +1725,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1663
1725
  const clip = data.data?.[0];
1664
1726
  if (!clip?.url) throw new Error("No audio URL in response");
1665
1727
  const txHash = resp.headers.get("X-Payment-Receipt") || resp.headers.get("x-payment-receipt");
1666
- recordSpending(budget, cost, agent_id);
1728
+ recordActualSpend(budget, billedUsd, cost, agent_id);
1667
1729
  const lines = [
1668
1730
  action === "sound_effect" ? `\u{1F50A} Sound effect ready!` : `\u{1F5E3}\uFE0F Speech ready!`,
1669
1731
  `URL: ${clip.url}`,
@@ -1671,7 +1733,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1671
1733
  ...clip.characters !== void 0 ? [`Characters: ${clip.characters}`] : [],
1672
1734
  ...clip.duration_seconds !== void 0 ? [`Duration: ${clip.duration_seconds}s`] : [],
1673
1735
  `Model: ${data.model || (action === "sound_effect" ? "elevenlabs/sound-effects" : model)}`,
1674
- `Cost: $${cost.toFixed(4)}`,
1736
+ `Cost: $${billedUsd.toFixed(4)}`,
1675
1737
  ...txHash ? [`Tx: ${txHash}`] : [],
1676
1738
  ``,
1677
1739
  `Note: This URL may expire \u2014 download it now if you need to keep the file.`
@@ -1684,7 +1746,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
1684
1746
  ...clip.characters !== void 0 ? { characters: clip.characters } : {},
1685
1747
  ...clip.duration_seconds !== void 0 ? { duration_seconds: clip.duration_seconds } : {},
1686
1748
  model: data.model || (action === "sound_effect" ? "elevenlabs/sound-effects" : model),
1687
- cost_usd: cost,
1749
+ cost_usd: billedUsd,
1688
1750
  ...txHash ? { txHash } : {}
1689
1751
  }
1690
1752
  };
@@ -1785,8 +1847,8 @@ function registerVideoTool(server, budget) {
1785
1847
  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
1848
 
1787
1849
  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
1850
+ - 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.
1851
+ - xai/grok-imagine-video ($0.05/sec, 8s default -> $0.40/clip) \u2014 stylized, fast
1790
1852
  - bytedance/seedance-1.5-pro (~$0.092/sec, 720p + audio t2v, 5s default up to 10s) \u2014 cheapest Seedance, token-priced upstream
1791
1853
  - 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
1854
  - 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 +1861,15 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1799
1861
  image_url: z7.string().url().optional().describe("Optional seed image URL for image-to-video generation"),
1800
1862
  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
1863
  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)."),
1864
+ 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."),
1865
+ 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."),
1866
+ 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."),
1867
+ 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
1868
  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
1869
  agent_id: z7.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1804
1870
  }
1805
1871
  },
1806
- async ({ prompt, image_url, real_face_asset_id, duration_seconds, model, agent_id }) => {
1872
+ async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
1807
1873
  try {
1808
1874
  if (getChain() !== "base") {
1809
1875
  return {
@@ -1826,6 +1892,20 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1826
1892
  };
1827
1893
  }
1828
1894
  }
1895
+ if (last_frame_url) {
1896
+ if (!image_url) {
1897
+ return {
1898
+ content: [{ type: "text", text: formatError("last_frame_url (first-and-last-frame interpolation) requires image_url as the first frame.") }],
1899
+ isError: true
1900
+ };
1901
+ }
1902
+ if (real_face_asset_id) {
1903
+ return {
1904
+ content: [{ type: "text", text: formatError("last_frame_url cannot be combined with real_face_asset_id.") }],
1905
+ isError: true
1906
+ };
1907
+ }
1908
+ }
1829
1909
  const billedSeconds = duration_seconds ?? VIDEO_DEFAULT_DURATION[selectedModel] ?? 8;
1830
1910
  const hasImageInput = Boolean(image_url || real_face_asset_id);
1831
1911
  const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
@@ -1844,6 +1924,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1844
1924
  if (image_url) body.image_url = image_url;
1845
1925
  if (real_face_asset_id) body.real_face_asset_id = real_face_asset_id;
1846
1926
  if (duration_seconds !== void 0) body.duration_seconds = duration_seconds;
1927
+ if (generate_audio !== void 0) body.generate_audio = generate_audio;
1928
+ if (resolution !== void 0) body.resolution = resolution;
1929
+ if (aspect_ratio !== void 0) body.aspect_ratio = aspect_ratio;
1930
+ if (last_frame_url) body.last_frame_url = last_frame_url;
1847
1931
  const resp402 = await fetchWithTimeout(submitUrl, {
1848
1932
  method: "POST",
1849
1933
  headers: { "Content-Type": "application/json" },
@@ -1857,6 +1941,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1857
1941
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
1858
1942
  const paymentRequired = parsePaymentRequired3(prHeader);
1859
1943
  const details = extractPaymentDetails3(paymentRequired);
1944
+ const settledUsd = amountToUsd(details.amount);
1860
1945
  const paymentPayload = await createPaymentPayload3(
1861
1946
  privateKey,
1862
1947
  account.address,
@@ -1939,7 +2024,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1939
2024
  ...completed.request_id ? [`Request ID: ${completed.request_id}`] : [],
1940
2025
  ...completed.txHash ? [`Tx: ${completed.txHash}`] : []
1941
2026
  ];
1942
- recordSpending(budget, estimatedCost, agent_id);
2027
+ recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
1943
2028
  return {
1944
2029
  content: [{ type: "text", text: lines.join("\n") }],
1945
2030
  structuredContent: {
@@ -2025,7 +2110,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
2025
2110
  body: reqBody
2026
2111
  }, 9e4);
2027
2112
  const data = await resp.json().catch(() => ({}));
2028
- return { status: resp.status, data };
2113
+ return { status: resp.status, data, settledUsd: amountToUsd(details.amount) };
2029
2114
  }
2030
2115
  function registerRealfaceTool(server, budget) {
2031
2116
  server.registerTool(
@@ -2188,7 +2273,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2188
2273
  if (!budgetCheck.allowed) {
2189
2274
  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
2275
  }
2191
- const { status, data } = await payAndPostJson(
2276
+ const { status, data, settledUsd } = await payAndPostJson(
2192
2277
  `${BLOCKRUN_API4}/v1/portrait/enroll`,
2193
2278
  JSON.stringify({ name, image_url }),
2194
2279
  "BlockRun Virtual Portrait enrollment"
@@ -2204,7 +2289,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2204
2289
  }
2205
2290
  const assetId = data.asset_id;
2206
2291
  if (!assetId) throw new Error(`Portrait response missing asset_id: ${JSON.stringify(data)}`);
2207
- recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2292
+ recordActualSpend(budget, settledUsd, ENROLLMENT_PRICE_USD, agent_id);
2208
2293
  const txHash = data.settlement?.tx_hash || void 0;
2209
2294
  const lines = [
2210
2295
  `\u2705 Virtual Portrait enrolled!`,
@@ -2255,6 +2340,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2255
2340
  if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
2256
2341
  const paymentRequired = parsePaymentRequired4(prHeader);
2257
2342
  const details = extractPaymentDetails4(paymentRequired);
2343
+ const settledUsd = amountToUsd(details.amount);
2258
2344
  const paymentPayload = await createPaymentPayload4(
2259
2345
  privateKey,
2260
2346
  account.address,
@@ -2291,7 +2377,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
2291
2377
  }
2292
2378
  const assetId = data.asset_id;
2293
2379
  if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
2294
- recordSpending(budget, ENROLLMENT_PRICE_USD, agent_id);
2380
+ recordActualSpend(budget, settledUsd, ENROLLMENT_PRICE_USD, agent_id);
2295
2381
  const txHash = data.settlement?.tx_hash || void 0;
2296
2382
  const lines = [
2297
2383
  `\u2705 RealFace enrolled!`,
@@ -2336,13 +2422,16 @@ import { z as z9 } from "zod";
2336
2422
  function coerceBody(body) {
2337
2423
  if (typeof body !== "string") return body;
2338
2424
  const trimmed = body.trim();
2339
- if (trimmed === "") return {};
2425
+ if (trimmed === "") return void 0;
2340
2426
  try {
2341
2427
  return JSON.parse(trimmed);
2342
2428
  } catch {
2343
2429
  return body;
2344
2430
  }
2345
2431
  }
2432
+ function asStructuredContent(result) {
2433
+ return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
2434
+ }
2346
2435
 
2347
2436
  // src/tools/search.ts
2348
2437
  var SEARCH_PRICE_PER_SOURCE = 0.025;
@@ -2389,7 +2478,7 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
2389
2478
  recordSpending(budget, estimatedCost, agent_id);
2390
2479
  return {
2391
2480
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2392
- structuredContent: result
2481
+ structuredContent: asStructuredContent(result)
2393
2482
  };
2394
2483
  } catch (err) {
2395
2484
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2447,7 +2536,7 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
2447
2536
  recordSpending(budget, estimatedCost, agent_id);
2448
2537
  return {
2449
2538
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2450
- structuredContent: result
2539
+ structuredContent: asStructuredContent(result)
2451
2540
  };
2452
2541
  } catch (err) {
2453
2542
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2544,12 +2633,11 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
2544
2633
  recordSpending(budget, estimatedCost, agent_id);
2545
2634
  return {
2546
2635
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2547
- structuredContent: result
2636
+ structuredContent: asStructuredContent(result)
2548
2637
  };
2549
2638
  } catch (err) {
2550
- const errMsg = err instanceof Error ? err.message : String(err);
2551
2639
  return {
2552
- content: [{ type: "text", text: formatError(errMsg) }],
2640
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
2553
2641
  isError: true
2554
2642
  };
2555
2643
  }
@@ -2674,9 +2762,8 @@ Examples:
2674
2762
  structuredContent: result
2675
2763
  };
2676
2764
  } catch (err) {
2677
- const msg = err instanceof Error ? err.message : String(err);
2678
2765
  return {
2679
- content: [{ type: "text", text: formatError(msg) }],
2766
+ content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
2680
2767
  isError: true
2681
2768
  };
2682
2769
  }
@@ -2809,7 +2896,7 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
2809
2896
  recordSpending(budget, estimatedCost, agent_id);
2810
2897
  return {
2811
2898
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2812
- structuredContent: result
2899
+ structuredContent: asStructuredContent(result)
2813
2900
  };
2814
2901
  } catch (err) {
2815
2902
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2875,7 +2962,7 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
2875
2962
  if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
2876
2963
  return {
2877
2964
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2878
- structuredContent: result
2965
+ structuredContent: asStructuredContent(result)
2879
2966
  };
2880
2967
  } catch (err) {
2881
2968
  return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
@@ -2973,7 +3060,7 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
2973
3060
  recordSpending(budget, estimatedCost, agent_id);
2974
3061
  return {
2975
3062
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2976
- structuredContent: result
3063
+ structuredContent: asStructuredContent(result)
2977
3064
  };
2978
3065
  } catch (err) {
2979
3066
  return {
@@ -3170,7 +3257,13 @@ function resolveTools(argv, env) {
3170
3257
 
3171
3258
  // src/mcp-handler.ts
3172
3259
  function initializeMcpServer(server, profileArgs) {
3173
- const budget = { limit: null, spent: 0, calls: 0, agents: /* @__PURE__ */ new Map() };
3260
+ const env = profileArgs?.env ?? process.env;
3261
+ const budget = {
3262
+ limit: parseBudgetLimitEnv(env.BLOCKRUN_BUDGET_LIMIT),
3263
+ spent: 0,
3264
+ calls: 0,
3265
+ agents: /* @__PURE__ */ new Map()
3266
+ };
3174
3267
  const modelCache = { models: null };
3175
3268
  const { profile, tools } = resolveTools(profileArgs?.argv, profileArgs?.env);
3176
3269
  const registrars = {
@@ -3243,9 +3336,15 @@ function looksLikeRawPrivateKey(value) {
3243
3336
  if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
3244
3337
  return false;
3245
3338
  }
3339
+ function looksLikeSolanaSecretKeyArray(value) {
3340
+ return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
3341
+ }
3246
3342
  function walk(obj, file, jsonPath, out) {
3247
3343
  if (obj === null || typeof obj !== "object") return;
3248
3344
  if (Array.isArray(obj)) {
3345
+ if (looksLikeSolanaSecretKeyArray(obj)) {
3346
+ out.push({ file, path: jsonPath || "(root)" });
3347
+ }
3249
3348
  obj.forEach((v, i) => walk(v, file, `${jsonPath}[${i}]`, out));
3250
3349
  return;
3251
3350
  }