@blockrun/mcp 0.14.2 → 0.14.3

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 (3) hide show
  1. package/README.md +14 -10
  2. package/dist/index.js +377 -85
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -129,7 +129,7 @@ Run `blockrun_wallet` to see your address. Send USDC on Base.
129
129
  | Coinbase | Send → USDC → Base network → paste address |
130
130
  | Bridge from Ethereum | [bridge.base.org](https://bridge.base.org) |
131
131
 
132
- $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~30 Seedance 1.5-pro clips (5s).
132
+ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~20 Seedance 1.5-pro clips (5s, ~$0.23 each).
133
133
 
134
134
  ---
135
135
 
@@ -139,7 +139,7 @@ $5 covers ~5,000 market queries, ~500 Exa searches, ~250 image generations, or ~
139
139
  |------|-------------|------|
140
140
  | `blockrun_chat` | 55+ LLMs (GPT, Claude, Gemini, DeepSeek, Kimi K2.6, GLM, NVIDIA free tier, ...) with `mode` tier routing | per token |
141
141
  | `blockrun_image` | DALL-E 3, GPT Image 1/2, Grok Imagine, Flux, CogView-4, Nano Banana — generation + editing | $0.015–0.12 |
142
- | `blockrun_video` | xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast | $0.03–0.30/sec |
142
+ | `blockrun_video` | xAI Grok Imagine Video + ByteDance Seedance 1.5/2.0/2.0-fast | $0.046–0.149/sec |
143
143
  | `blockrun_music` | MiniMax music generation | per track |
144
144
  | `blockrun_price` | Pyth-backed realtime + OHLC — crypto / FX / commodity (free), 12 stock markets (paid) | free or $0.001/call |
145
145
  | `blockrun_markets` | Polymarket (markets, candles, trades, orderbooks, leaderboards, smart-wallet PnL/clusters, UMA oracle), Kalshi, Limitless, Opinion, Predict.Fun, dFlow, Binance Futures, cross-platform match + search | $0.001–0.005/query |
@@ -230,7 +230,7 @@ Delegate a spending budget to a child agent with `agent_id`. The child is auto-b
230
230
  ## Troubleshooting
231
231
 
232
232
  - **`Insufficient balance` / HTTP 402 after retry** → Run `blockrun_wallet action:"setup"`. Send USDC on Base (or Solana — see [Environment Variables](#environment-variables)).
233
- - **`Smart routing (ClawRouter) is not available on Solana`** → Pass `model:` or `mode:` explicitly to `blockrun_chat`, or switch back to Base by unsetting `SOLANA_WALLET_KEY` and removing `~/.blockrun/.solana-session`.
233
+ - **`Smart routing (ClawRouter) is not available on Solana`** → Pass `model:` or `mode:` explicitly to `blockrun_chat`, or switch back to Base with `echo base > ~/.blockrun/.chain`.
234
234
  - **`claude mcp list` doesn't show `blockrun`** → Check `node -v` (must be ≥18). Clear the npx cache: `rm -rf ~/.npm/_npx`. Re-run the install command from above.
235
235
  - **`fetch failed` / timeout when checking wallet balance** → Base RPC transient outage. The tool already falls through 3 public RPCs; retry after 30s. Persistent failures usually = local proxy / firewall blocking outbound RPC.
236
236
  - **`ENOENT: ~/.blockrun/.session`** → Expected on first run. The server auto-creates the wallet; check stderr for the `WALLET_CREATED` line confirming the address.
@@ -244,19 +244,23 @@ Delegate a spending budget to a child agent with `agent_id`. The child is auto-b
244
244
  | Variable / File | Default | Effect |
245
245
  |---|---|---|
246
246
  | `~/.blockrun/.session` | auto-created on first run | EVM private key (0x...). File exists → use Base. |
247
- | `~/.blockrun/.solana-session` | not created | Solana private key. File exists switch to Solana. |
247
+ | `~/.blockrun/.chain` | unset | Optional explicit chain preference: `base` or `solana`. |
248
+ | `~/.blockrun/.solana-session` | not created | Solana private key. File exists → switch to Solana unless `.chain` says `base`. |
248
249
  | `SOLANA_WALLET_KEY` | unset | Env-var override of `.solana-session`. Set → use Solana. |
249
250
 
250
- Chain selection priority (see `src/utils/wallet.ts:24`):
251
+ Chain selection priority (see `src/utils/wallet.ts`):
251
252
 
252
- 1. `SOLANA_WALLET_KEY` env var presentSolana
253
- 2. `~/.blockrun/.solana-session` exists → Solana
254
- 3. Otherwise → Base (`~/.blockrun/.session` auto-created)
253
+ 1. `~/.blockrun/.chain` or `~/.blockrun/payment-chain` set to `base` or `solana` explicit preference wins
254
+ 2. `SOLANA_WALLET_KEY` env var present → Solana
255
+ 3. `~/.blockrun/.solana-session` exists → Solana
256
+ 4. Otherwise → Base (`~/.blockrun/.session` auto-created)
255
257
 
256
258
  **Switching chains:**
257
259
 
258
- - Base → Solana: `export SOLANA_WALLET_KEY=...`, or `echo "<secret>" > ~/.blockrun/.solana-session`
259
- - Solana → Base: `unset SOLANA_WALLET_KEY && rm ~/.blockrun/.solana-session` (the existing `.session` is reused, so it's the same Base wallet)
260
+ - Base → Solana: `echo solana > ~/.blockrun/.chain`, then set `SOLANA_WALLET_KEY` or create `~/.blockrun/.solana-session`
261
+ - Solana → Base: `echo base > ~/.blockrun/.chain` (the existing `.session` is reused, so it's the same Base wallet)
262
+
263
+ Some media and paid market-data tools still settle on Base only: `blockrun_image`, `blockrun_music`, `blockrun_video`, and paid stock `blockrun_price` calls. In Solana mode they fail before creating or charging a Base wallet.
260
264
 
261
265
  The server also runs a non-blocking npm registry check at startup and prints an `Update available` notice to stderr when a newer `@blockrun/mcp` version exists. Upgrade by re-running the install command — no manual `npm update` needed.
262
266
 
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var CHAIN_PREFERENCE_FILES = [
29
29
  var _evmClient = null;
30
30
  var _imageClient = null;
31
31
  var _priceClient = null;
32
+ var _freePriceClient = null;
32
33
  var _evmWalletInfo = null;
33
34
  var _solanaClient = null;
34
35
  function readChainPreference() {
@@ -86,7 +87,13 @@ function getImageClient() {
86
87
  }
87
88
  return _imageClient;
88
89
  }
89
- function getPriceClient() {
90
+ function getPriceClient(requireWallet = true) {
91
+ if (!requireWallet) {
92
+ if (!_freePriceClient) {
93
+ _freePriceClient = new PriceClient({ requireWallet: false });
94
+ }
95
+ return _freePriceClient;
96
+ }
90
97
  if (!_priceClient) {
91
98
  const privateKey = getOrCreateWalletKey();
92
99
  _priceClient = new PriceClient({ privateKey });
@@ -331,9 +338,6 @@ Do NOT call this for actual AI queries \u2014 use blockrun_chat for that.`,
331
338
  }
332
339
  },
333
340
  async ({ action, budget_action, budget_amount, agent_id, agent_limit }) => {
334
- const info = await getWalletInfo();
335
- const address = info.address;
336
- const chain = getChain();
337
341
  if (action === "budget") {
338
342
  const budgetAct = budget_action || "check";
339
343
  if (budgetAct === "set") {
@@ -409,6 +413,9 @@ Pass agent_id: "${agent_id}" in any blockrun_* tool call to track and enforce th
409
413
  structuredContent: { global: { limit: budget.limit, spent: budget.spent, calls: budget.calls }, agents: agentRows }
410
414
  };
411
415
  }
416
+ const info = await getWalletInfo();
417
+ const address = info.address;
418
+ const chain = getChain();
412
419
  if (action === "qr") {
413
420
  try {
414
421
  const qrPath = await generateQrPng(address, chain);
@@ -532,19 +539,26 @@ import { z as z2 } from "zod";
532
539
  import { LLMClient as LLMClient2 } from "@blockrun/llm";
533
540
 
534
541
  // src/utils/budget.ts
535
- function checkBudget(budget, agentId) {
536
- if (budget.limit !== null && budget.spent >= budget.limit) {
542
+ var EPSILON = 1e-9;
543
+ function formatUsd(amount) {
544
+ return `$${amount.toFixed(amount >= 1 ? 2 : 4)}`;
545
+ }
546
+ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
547
+ const cost = Math.max(0, estimatedCost);
548
+ if (cost > 0 && budget.limit !== null && budget.spent + cost > budget.limit + EPSILON) {
549
+ const remaining = Math.max(0, budget.limit - budget.spent);
537
550
  return {
538
551
  allowed: false,
539
- reason: `Global budget limit $${budget.limit.toFixed(2)} reached ($${budget.spent.toFixed(4)} spent)`
552
+ reason: `Global budget limit ${formatUsd(budget.limit)} would be exceeded (${formatUsd(budget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
540
553
  };
541
554
  }
542
555
  if (agentId) {
543
556
  const agentBudget = budget.agents.get(agentId);
544
- if (agentBudget && agentBudget.spent >= agentBudget.limit) {
557
+ if (cost > 0 && agentBudget && agentBudget.spent + cost > agentBudget.limit + EPSILON) {
558
+ const remaining = Math.max(0, agentBudget.limit - agentBudget.spent);
545
559
  return {
546
560
  allowed: false,
547
- reason: `Agent "${agentId}" budget $${agentBudget.limit.toFixed(2)} exhausted ($${agentBudget.spent.toFixed(4)} spent in ${agentBudget.calls} calls)`
561
+ reason: `Agent "${agentId}" budget ${formatUsd(agentBudget.limit)} would be exceeded (${formatUsd(agentBudget.spent)} spent, ${formatUsd(remaining)} remaining, next call estimated ${formatUsd(cost)})`
548
562
  };
549
563
  }
550
564
  }
@@ -563,6 +577,24 @@ function recordSpending(budget, cost, agentId) {
563
577
  }
564
578
 
565
579
  // src/tools/chat.ts
580
+ function estimateChatCost(mode, model, routing, routingProfile) {
581
+ if (mode === "free") return 0;
582
+ if (model?.startsWith("nvidia/")) return 0;
583
+ if (routing === "smart" && routingProfile === "free") return 0;
584
+ if (routing === "smart") {
585
+ switch (routingProfile) {
586
+ case "eco":
587
+ return 2e-3;
588
+ case "premium":
589
+ return 0.05;
590
+ case "auto":
591
+ default:
592
+ return 0.01;
593
+ }
594
+ }
595
+ if (mode === "reasoning" || mode === "powerful") return 0.01;
596
+ return 1e-3;
597
+ }
566
598
  function registerChatTool(server, budget) {
567
599
  server.registerTool(
568
600
  "blockrun_chat",
@@ -598,7 +630,8 @@ Run blockrun_models to see all 41+ models with pricing.`,
598
630
  },
599
631
  async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, agent_id, messages }) => {
600
632
  const llm = getClient();
601
- const budgetCheck = checkBudget(budget, agent_id);
633
+ const estimatedCost = estimateChatCost(mode, model, routing, routing_profile);
634
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
602
635
  if (!budgetCheck.allowed) {
603
636
  return {
604
637
  content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
@@ -616,6 +649,7 @@ Run blockrun_models to see all 41+ models with pricing.`,
616
649
  const result = await llm.smartChat(message, {
617
650
  system,
618
651
  maxTokens: max_tokens,
652
+ maxOutputTokens: max_tokens,
619
653
  temperature,
620
654
  routingProfile: routing_profile
621
655
  });
@@ -648,7 +682,7 @@ ${result.response}` }],
648
682
  temperature
649
683
  });
650
684
  const reply = result.choices?.[0]?.message?.content || "";
651
- recordSpending(budget, 1e-3, agent_id);
685
+ recordSpending(budget, estimatedCost, agent_id);
652
686
  return {
653
687
  content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
654
688
 
@@ -667,7 +701,7 @@ ${reply}` }],
667
701
  maxTokens: max_tokens,
668
702
  temperature
669
703
  });
670
- recordSpending(budget, 1e-3, agent_id);
704
+ recordSpending(budget, estimatedCost, agent_id);
671
705
  return { content: [{ type: "text", text: response }] };
672
706
  } catch (error) {
673
707
  const errorMessage2 = error instanceof Error ? error.message : String(error);
@@ -684,9 +718,10 @@ ${reply}` }],
684
718
  try {
685
719
  const response = await llm.chat(m, message, {
686
720
  system,
687
- maxTokens: max_tokens
721
+ maxTokens: max_tokens,
722
+ temperature
688
723
  });
689
- recordSpending(budget, 1e-3, agent_id);
724
+ recordSpending(budget, estimatedCost, agent_id);
690
725
  return {
691
726
  content: [{ type: "text", text: `[${m}]
692
727
 
@@ -709,6 +744,9 @@ ${response}` }],
709
744
 
710
745
  // src/tools/models.ts
711
746
  import { z as z3 } from "zod";
747
+ function getModelType(model) {
748
+ return model.type === "image" || "pricePerImage" in model ? "image" : "llm";
749
+ }
712
750
  function registerModelsTool(server, modelCache) {
713
751
  server.registerTool(
714
752
  "blockrun_models",
@@ -722,7 +760,7 @@ function registerModelsTool(server, modelCache) {
722
760
  async ({ category, provider }) => {
723
761
  const llm = getClient();
724
762
  if (!modelCache.models) {
725
- modelCache.models = await llm.listModels();
763
+ modelCache.models = "listAllModels" in llm ? await llm.listAllModels() : await llm.listModels();
726
764
  setTimeout(() => {
727
765
  modelCache.models = null;
728
766
  }, 5 * 60 * 1e3);
@@ -734,20 +772,27 @@ function registerModelsTool(server, modelCache) {
734
772
  }
735
773
  if (category && category !== "all") {
736
774
  if (category === "image") {
737
- models = models.filter((m) => m.id.includes("dall-e") || m.id.includes("flux") || m.id.includes("banana"));
775
+ models = models.filter((m) => getModelType(m) === "image");
738
776
  } else if (category === "embedding") {
739
777
  models = models.filter((m) => m.id.includes("embed"));
740
778
  } else {
741
- models = models.filter((m) => m.categories?.includes(category));
779
+ models = models.filter((m) => "categories" in m && m.categories?.includes(category));
742
780
  }
743
781
  }
744
782
  const lines = models.map((m) => {
745
- const input = m.inputPrice ? `$${m.inputPrice}/M in` : "";
746
- const output = m.outputPrice ? `$${m.outputPrice}/M out` : "";
783
+ if (getModelType(m) === "image") {
784
+ const image = m;
785
+ const pricing2 = image.pricePerImage ? `$${image.pricePerImage}/image` : "";
786
+ const sizes = image.supportedSizes?.length ? ` | sizes: ${image.supportedSizes.join(", ")}` : "";
787
+ return `- ${image.id}${pricing2 ? ` (${pricing2})` : ""}${sizes} [image]`;
788
+ }
789
+ const llmModel = m;
790
+ const input = llmModel.inputPrice ? `$${llmModel.inputPrice}/M in` : "";
791
+ const output = llmModel.outputPrice ? `$${llmModel.outputPrice}/M out` : "";
747
792
  const pricing = [input, output].filter(Boolean).join(", ");
748
- const ctx = m.contextWindow ? ` | ${Math.round(m.contextWindow / 1e3)}K ctx` : "";
749
- const cats = m.categories?.length ? ` [${m.categories.join(", ")}]` : "";
750
- return `- ${m.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
793
+ const ctx = llmModel.contextWindow ? ` | ${Math.round(llmModel.contextWindow / 1e3)}K ctx` : "";
794
+ const cats = llmModel.categories?.length ? ` [${llmModel.categories.join(", ")}]` : "";
795
+ return `- ${llmModel.id}${pricing ? ` (${pricing})` : ""}${ctx}${cats}`;
751
796
  });
752
797
  return {
753
798
  content: [{ type: "text", text: `Models (${models.length}):
@@ -761,7 +806,18 @@ ${lines.join("\n")}` }],
761
806
  // src/tools/image.ts
762
807
  import { z as z4 } from "zod";
763
808
  import { PaymentError } from "@blockrun/llm";
764
- function registerImageTool(server) {
809
+ var GENERATE_MODEL_COST = {
810
+ "zai/cogview-4": 0.015,
811
+ "xai/grok-imagine-image": 0.02,
812
+ "xai/grok-imagine-image-pro": 0.07,
813
+ "openai/gpt-image-1": 0.04,
814
+ "openai/gpt-image-2": 0.12,
815
+ "openai/dall-e-3": 0.08,
816
+ "google/nano-banana": 0.05,
817
+ "together/flux-schnell": 3e-3
818
+ };
819
+ var EDIT_MODELS = /* @__PURE__ */ new Set(["openai/gpt-image-1", "openai/gpt-image-2"]);
820
+ function registerImageTool(server, budget) {
765
821
  server.registerTool(
766
822
  "blockrun_image",
767
823
  {
@@ -786,12 +842,18 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
786
842
  model: z4.enum(["zai/cogview-4", "openai/dall-e-3", "together/flux-schnell", "google/nano-banana", "openai/gpt-image-1", "openai/gpt-image-2", "xai/grok-imagine-image", "xai/grok-imagine-image-pro"]).optional().describe("Model to use (default: dall-e-3 for generate, gpt-image-2 for edit). xai/grok-imagine-image is stylized and fast; xai/grok-imagine-image-pro is higher quality; gpt-image-2 is the newest edit-capable model with stronger instruction following."),
787
843
  image: z4.string().optional().describe("Source image for edit action: base64-encoded image or URL"),
788
844
  size: z4.enum(["1024x1024", "1792x1024", "1024x1792"]).optional().default("1024x1024"),
789
- quality: z4.enum(["standard", "hd"]).optional().default("standard")
845
+ quality: z4.enum(["standard", "hd"]).optional().default("standard"),
846
+ agent_id: z4.string().optional().describe("Agent identifier for budget tracking and enforcement.")
790
847
  }
791
848
  },
792
- async ({ prompt, action, model, image, size, quality }) => {
849
+ async ({ prompt, action, model, image, size, quality, agent_id }) => {
793
850
  try {
794
- const imgClient = getImageClient();
851
+ if (getChain() !== "base") {
852
+ return {
853
+ content: [{ type: "text", text: formatError("blockrun_image currently settles on Base only. Switch BlockRun to Base (for example: write 'base' to ~/.blockrun/.chain) and fund the Base wallet with USDC.") }],
854
+ isError: true
855
+ };
856
+ }
795
857
  let response;
796
858
  if (action === "edit") {
797
859
  if (!image) {
@@ -800,16 +862,42 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
800
862
  isError: true
801
863
  };
802
864
  }
803
- response = await imgClient.edit(prompt, image, {
804
- model: model || "openai/gpt-image-2",
865
+ const selectedModel = model || "openai/gpt-image-2";
866
+ if (!EDIT_MODELS.has(selectedModel)) {
867
+ return {
868
+ content: [{ type: "text", text: formatError("Image edits support only openai/gpt-image-1 or openai/gpt-image-2") }],
869
+ isError: true
870
+ };
871
+ }
872
+ const estimatedCost = selectedModel === "openai/gpt-image-2" ? 0.12 : 0.04;
873
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
874
+ if (!budgetCheck.allowed) {
875
+ return {
876
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
877
+ isError: true
878
+ };
879
+ }
880
+ response = await getImageClient().edit(prompt, image, {
881
+ model: selectedModel,
805
882
  size
806
883
  });
884
+ recordSpending(budget, estimatedCost, agent_id);
807
885
  } else {
808
- response = await imgClient.generate(prompt, {
809
- model: model || "openai/dall-e-3",
886
+ const selectedModel = model || "openai/dall-e-3";
887
+ const estimatedCost = GENERATE_MODEL_COST[selectedModel] ?? 0.05;
888
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
889
+ if (!budgetCheck.allowed) {
890
+ return {
891
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
892
+ isError: true
893
+ };
894
+ }
895
+ response = await getImageClient().generate(prompt, {
896
+ model: selectedModel,
810
897
  size,
811
898
  quality
812
899
  });
900
+ recordSpending(budget, estimatedCost, agent_id);
813
901
  }
814
902
  const imageUrl = response.data?.[0]?.url;
815
903
  if (!imageUrl) {
@@ -821,8 +909,8 @@ Edit models: openai/gpt-image-1, openai/gpt-image-2 (default for edits)`,
821
909
  return {
822
910
  content: [{ type: "text", text: `Image: ${imageUrl}
823
911
  Prompt: ${prompt}
824
- Model: ${model}` }],
825
- structuredContent: { url: imageUrl, prompt, model }
912
+ Model: ${action === "edit" ? model || "openai/gpt-image-2" : model || "openai/dall-e-3"}` }],
913
+ structuredContent: { url: imageUrl, prompt, model: action === "edit" ? model || "openai/gpt-image-2" : model || "openai/dall-e-3" }
826
914
  };
827
915
  } catch (err) {
828
916
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -852,7 +940,8 @@ import {
852
940
  } from "@blockrun/llm";
853
941
  var BLOCKRUN_API = "https://blockrun.ai/api";
854
942
  var MUSIC_TIMEOUT = 2e5;
855
- function registerMusicTool(server) {
943
+ var MUSIC_COST = 0.1575;
944
+ function registerMusicTool(server, budget) {
856
945
  server.registerTool(
857
946
  "blockrun_music",
858
947
  {
@@ -867,17 +956,31 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
867
956
  prompt: z5.string().describe("Music style, mood, or description. E.g. 'upbeat synthwave with neon pads', 'chill lo-fi beats', 'epic orchestral film score'"),
868
957
  instrumental: z5.boolean().optional().default(true).describe("Generate without vocals (default: true)"),
869
958
  lyrics: z5.string().optional().describe("Custom lyrics. Cannot be used with instrumental: true"),
870
- model: z5.enum(["minimax/music-2.5+", "minimax/music-2.5"]).optional().default("minimax/music-2.5+").describe("Music model to use")
959
+ model: z5.enum(["minimax/music-2.5+", "minimax/music-2.5"]).optional().default("minimax/music-2.5+").describe("Music model to use"),
960
+ agent_id: z5.string().optional().describe("Agent identifier for budget tracking and enforcement.")
871
961
  }
872
962
  },
873
- async ({ prompt, instrumental, lyrics, model }) => {
963
+ async ({ prompt, instrumental, lyrics, model, agent_id }) => {
874
964
  try {
965
+ if (getChain() !== "base") {
966
+ return {
967
+ content: [{ type: "text", text: formatError("blockrun_music currently settles on Base only. Switch BlockRun to Base (for example: write 'base' to ~/.blockrun/.chain) and fund the Base wallet with USDC.") }],
968
+ isError: true
969
+ };
970
+ }
875
971
  if (instrumental && lyrics?.trim()) {
876
972
  return {
877
973
  content: [{ type: "text", text: formatError("Cannot specify lyrics when instrumental is true") }],
878
974
  isError: true
879
975
  };
880
976
  }
977
+ const budgetCheck = checkBudget(budget, agent_id, MUSIC_COST);
978
+ if (!budgetCheck.allowed) {
979
+ return {
980
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
981
+ isError: true
982
+ };
983
+ }
881
984
  const privateKey = getOrCreateWalletKey();
882
985
  const account = privateKeyToAccount(privateKey);
883
986
  const url = `${BLOCKRUN_API}/v1/audio/generations`;
@@ -928,6 +1031,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
928
1031
  const track = data.data?.[0];
929
1032
  if (!track?.url) throw new Error("No track URL in response");
930
1033
  const txHash = resp.headers.get("X-Payment-Receipt") || resp.headers.get("x-payment-receipt");
1034
+ recordSpending(budget, MUSIC_COST, agent_id);
931
1035
  const lines = [
932
1036
  `\u{1F3B5} Track ready!`,
933
1037
  `URL: ${track.url}`,
@@ -993,7 +1097,19 @@ import {
993
1097
  var BLOCKRUN_API2 = "https://blockrun.ai/api";
994
1098
  var TOTAL_BUDGET_MS = 3e5;
995
1099
  var POLL_INTERVAL_MS = 5e3;
996
- function registerVideoTool(server) {
1100
+ var VIDEO_PRICE_PER_SECOND = {
1101
+ "xai/grok-imagine-video": 0.05,
1102
+ "bytedance/seedance-1.5-pro": 0.046,
1103
+ "bytedance/seedance-2.0-fast": 0.119,
1104
+ "bytedance/seedance-2.0": 0.149
1105
+ };
1106
+ var VIDEO_DEFAULT_DURATION = {
1107
+ "xai/grok-imagine-video": 8,
1108
+ "bytedance/seedance-1.5-pro": 5,
1109
+ "bytedance/seedance-2.0-fast": 5,
1110
+ "bytedance/seedance-2.0": 5
1111
+ };
1112
+ function registerVideoTool(server, budget) {
997
1113
  server.registerTool(
998
1114
  "blockrun_video",
999
1115
  {
@@ -1003,24 +1119,41 @@ Turns a text prompt (and optional seed image) into a short MP4 clip. The tool su
1003
1119
 
1004
1120
  Models:
1005
1121
  - xai/grok-imagine-video ($0.05/sec, 8s default -> $0.42/clip) \u2014 stylized, fast
1006
- - bytedance/seedance-1.5-pro ($0.03/sec, 720p, 5s default up to 10s) \u2014 cheapest
1007
- - bytedance/seedance-2.0-fast ($0.15/sec, ~60-80s gen) \u2014 sweet-spot price/quality
1008
- - bytedance/seedance-2.0 ($0.30/sec, 720p Pro) \u2014 highest quality
1122
+ - bytedance/seedance-1.5-pro (~$0.046/sec, 480p, 5s default up to 10s) \u2014 cheapest, token-priced upstream
1123
+ - bytedance/seedance-2.0-fast (~$0.119/sec text \xB7 ~$0.07/sec image-to-video, ~60-80s gen) \u2014 sweet-spot price/quality
1124
+ - bytedance/seedance-2.0 (~$0.149/sec text \xB7 ~$0.092/sec image-to-video, 480p Pro) \u2014 highest quality
1009
1125
 
1010
1126
  Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GCS so URLs don't expire).`,
1011
1127
  inputSchema: {
1012
1128
  prompt: z6.string().describe("Text description of the video to generate. E.g. 'a red apple slowly spinning on a wooden table', 'a hummingbird hovering near a red flower, ultra slow motion'"),
1013
1129
  image_url: z6.string().url().optional().describe("Optional seed image URL for image-to-video generation"),
1014
1130
  duration_seconds: z6.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)."),
1015
- model: z6.enum(["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")
1131
+ model: z6.enum(["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"),
1132
+ agent_id: z6.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1016
1133
  }
1017
1134
  },
1018
- async ({ prompt, image_url, duration_seconds, model }) => {
1135
+ async ({ prompt, image_url, duration_seconds, model, agent_id }) => {
1019
1136
  try {
1137
+ if (getChain() !== "base") {
1138
+ return {
1139
+ content: [{ type: "text", text: formatError("blockrun_video currently settles on Base only. Switch BlockRun to Base (for example: write 'base' to ~/.blockrun/.chain) and fund the Base wallet with USDC.") }],
1140
+ isError: true
1141
+ };
1142
+ }
1143
+ const selectedModel = model || "xai/grok-imagine-video";
1144
+ const billedSeconds = duration_seconds ?? VIDEO_DEFAULT_DURATION[selectedModel] ?? 8;
1145
+ const estimatedCost = (VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05) * billedSeconds;
1146
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1147
+ if (!budgetCheck.allowed) {
1148
+ return {
1149
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1150
+ isError: true
1151
+ };
1152
+ }
1020
1153
  const privateKey = getOrCreateWalletKey();
1021
1154
  const account = privateKeyToAccount2(privateKey);
1022
1155
  const submitUrl = `${BLOCKRUN_API2}/v1/videos/generations`;
1023
- const body = { model, prompt };
1156
+ const body = { model: selectedModel, prompt };
1024
1157
  if (image_url) body.image_url = image_url;
1025
1158
  if (duration_seconds !== void 0) body.duration_seconds = duration_seconds;
1026
1159
  const resp402 = await fetchWithTimeout2(submitUrl, {
@@ -1113,18 +1246,19 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
1113
1246
  `\u{1F3AC} Video ready!`,
1114
1247
  `URL: ${completed.url}`,
1115
1248
  `Duration: ${completed.duration_seconds ? `${completed.duration_seconds}s` : "8s"}`,
1116
- `Model: ${completed.modelReturned || model}`,
1249
+ `Model: ${completed.modelReturned || selectedModel}`,
1117
1250
  ...completed.backed_up ? [`Backed up to BlockRun storage (URL is permanent)`] : completed.source_url ? [`Source URL: ${completed.source_url}`] : [],
1118
1251
  ...completed.request_id ? [`Request ID: ${completed.request_id}`] : [],
1119
1252
  ...completed.txHash ? [`Tx: ${completed.txHash}`] : []
1120
1253
  ];
1254
+ recordSpending(budget, estimatedCost, agent_id);
1121
1255
  return {
1122
1256
  content: [{ type: "text", text: lines.join("\n") }],
1123
1257
  structuredContent: {
1124
1258
  url: completed.url,
1125
1259
  ...completed.source_url ? { source_url: completed.source_url } : {},
1126
1260
  duration_seconds: completed.duration_seconds,
1127
- model: completed.modelReturned || model,
1261
+ model: completed.modelReturned || selectedModel,
1128
1262
  ...completed.request_id ? { request_id: completed.request_id } : {},
1129
1263
  ...completed.backed_up !== void 0 ? { backed_up: completed.backed_up } : {},
1130
1264
  ...completed.txHash ? { txHash: completed.txHash } : {}
@@ -1166,29 +1300,47 @@ async function fetchWithTimeout2(url, options, timeoutMs) {
1166
1300
 
1167
1301
  // src/tools/search.ts
1168
1302
  import { z as z7 } from "zod";
1169
- function registerSearchTool(server) {
1303
+ var SEARCH_PRICE_PER_SOURCE = 0.025;
1304
+ var SEARCH_DEFAULT_MAX_RESULTS = 10;
1305
+ function estimateSearchCost(body) {
1306
+ if (!body || typeof body !== "object") return SEARCH_PRICE_PER_SOURCE * SEARCH_DEFAULT_MAX_RESULTS;
1307
+ const raw = body.max_results;
1308
+ const max = typeof raw === "number" && raw > 0 ? Math.min(50, Math.floor(raw)) : SEARCH_DEFAULT_MAX_RESULTS;
1309
+ return SEARCH_PRICE_PER_SOURCE * max;
1310
+ }
1311
+ function registerSearchTool(server, budget) {
1170
1312
  server.registerTool(
1171
1313
  "blockrun_search",
1172
1314
  {
1173
- description: `Grok Live Search \u2014 real-time web + X/Twitter + news with AI-summarized results and citations. ~$0.025 per source.
1315
+ description: `Grok Live Search \u2014 real-time web + X/Twitter + news with AI-summarized results and citations. $0.025 per returned source (max_results \xD7 $0.025; default max_results=10 \u2192 $0.25).
1174
1316
 
1175
1317
  Common shape:
1176
- - body: { query: "...", sources: ["web","x","news"], maxResults: 10, fromDate: "YYYY-MM-DD", toDate: "YYYY-MM-DD" }
1318
+ - body: { query: "...", sources: ["web","x","news"], max_results: 10, from_date: "YYYY-MM-DD", to_date: "YYYY-MM-DD" }
1177
1319
 
1178
- \`sources\` accepts any subset of ["web","x","news"] (defaults to all three). For tweet-only searches, use ["x"].
1320
+ \`sources\` accepts any subset of ["web","x","news"] (defaults to all three). For tweet-only searches, use ["x"]. \`max_results\` is 1\u201350 (default 10) and drives the price \u2014 pass a smaller value if you want to cap spend.
1179
1321
 
1180
1322
  Full request shape + worked examples in the \`search\` skill (\`skills/search/SKILL.md\`).`,
1181
1323
  inputSchema: {
1182
1324
  path: z7.string().optional().default("").describe("Endpoint sub-path under /v1/search/ (default empty = root /v1/search). Reserved for future surfaces."),
1183
- body: z7.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST.")
1325
+ body: z7.any().optional().describe("Request body. At minimum { query: '...' }. Sent as POST."),
1326
+ agent_id: z7.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1184
1327
  }
1185
1328
  },
1186
- async ({ path: path4, body }) => {
1329
+ async ({ path: path4, body, agent_id }) => {
1187
1330
  try {
1331
+ const estimatedCost = estimateSearchCost(body);
1332
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1333
+ if (!budgetCheck.allowed) {
1334
+ return {
1335
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1336
+ isError: true
1337
+ };
1338
+ }
1188
1339
  const client = getClient();
1189
1340
  const cleanPath = (path4 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
1190
1341
  const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
1191
1342
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1343
+ recordSpending(budget, estimatedCost, agent_id);
1192
1344
  return {
1193
1345
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1194
1346
  structuredContent: result
@@ -1202,7 +1354,15 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
1202
1354
 
1203
1355
  // src/tools/exa.ts
1204
1356
  import { z as z8 } from "zod";
1205
- function registerExaTool(server) {
1357
+ function estimateExaCost(path4, body) {
1358
+ const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1359
+ if (cleanPath === "contents") {
1360
+ const urls = body && typeof body === "object" ? body.urls : void 0;
1361
+ return 2e-3 * (Array.isArray(urls) && urls.length > 0 ? urls.length : 1);
1362
+ }
1363
+ return 0.01;
1364
+ }
1365
+ function registerExaTool(server, budget) {
1206
1366
  server.registerTool(
1207
1367
  "blockrun_exa",
1208
1368
  {
@@ -1219,15 +1379,25 @@ Categories for search: "news", "research paper", "company", "tweet", "github", "
1219
1379
  Full request/response shapes + worked research workflows in the \`exa-research\` skill.`,
1220
1380
  inputSchema: {
1221
1381
  path: z8.string().describe("Endpoint name under /v1/exa/, e.g. 'search', 'answer', 'contents', 'find-similar'"),
1222
- body: z8.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints.")
1382
+ body: z8.any().optional().describe("JSON body for the call. Sent as POST. Required for all four endpoints."),
1383
+ agent_id: z8.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1223
1384
  }
1224
1385
  },
1225
- async ({ path: path4, body }) => {
1386
+ async ({ path: path4, body, agent_id }) => {
1226
1387
  try {
1388
+ const estimatedCost = estimateExaCost(path4, body);
1389
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1390
+ if (!budgetCheck.allowed) {
1391
+ return {
1392
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1393
+ isError: true
1394
+ };
1395
+ }
1227
1396
  const client = getClient();
1228
1397
  const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
1229
1398
  const endpoint = `/v1/exa/${cleanPath}`;
1230
1399
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1400
+ recordSpending(budget, estimatedCost, agent_id);
1231
1401
  return {
1232
1402
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1233
1403
  structuredContent: result
@@ -1241,7 +1411,15 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
1241
1411
 
1242
1412
  // src/tools/markets.ts
1243
1413
  import { z as z9 } from "zod";
1244
- function registerMarketsTool(server) {
1414
+ function estimateMarketCost(path4, body) {
1415
+ if (body !== void 0) return 5e-3;
1416
+ const p = path4.toLowerCase();
1417
+ if (p.includes("wallet") || p.includes("smart") || p.includes("matching-markets") || p.includes("markets/search") || p.includes("binance/")) {
1418
+ return 5e-3;
1419
+ }
1420
+ return 1e-3;
1421
+ }
1422
+ function registerMarketsTool(server, budget) {
1245
1423
  server.registerTool(
1246
1424
  "blockrun_markets",
1247
1425
  {
@@ -1299,13 +1477,23 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
1299
1477
  inputSchema: {
1300
1478
  path: z9.string().describe("Endpoint path, e.g. 'polymarket/events', 'kalshi/markets/KXBTC-25MAR14', 'polymarket/wallet/0xabc...', 'markets/search'"),
1301
1479
  params: z9.record(z9.string(), z9.string()).optional().describe("Query parameters for GET requests (e.g. { limit: '20', active: 'true' })"),
1302
- body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)")
1480
+ body: z9.any().optional().describe("JSON body for POST queries (triggers pmQuery \u2014 most endpoints are GET)"),
1481
+ agent_id: z9.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1303
1482
  }
1304
1483
  },
1305
- async ({ path: path4, params, body }) => {
1484
+ async ({ path: path4, params, body, agent_id }) => {
1306
1485
  try {
1486
+ const estimatedCost = estimateMarketCost(path4, body);
1487
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1488
+ if (!budgetCheck.allowed) {
1489
+ return {
1490
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1491
+ isError: true
1492
+ };
1493
+ }
1307
1494
  const llm = getClient();
1308
- const result = body ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
1495
+ const result = body !== void 0 ? await llm.pmQuery(path4, body) : await llm.pm(path4, params);
1496
+ recordSpending(budget, estimatedCost, agent_id);
1309
1497
  return {
1310
1498
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1311
1499
  structuredContent: result
@@ -1341,7 +1529,10 @@ var MARKET = z10.enum([
1341
1529
  var RESOLUTION = z10.enum(["1", "5", "15", "60", "240", "D", "W", "M"]);
1342
1530
  var SESSION = z10.enum(["pre", "post", "on"]);
1343
1531
  var ACTION = z10.enum(["price", "history", "list"]);
1344
- function registerPriceTool(server) {
1532
+ function isPaidPriceCall(action, category) {
1533
+ return action !== "list" && (category === "stocks" || category === "usstock");
1534
+ }
1535
+ function registerPriceTool(server, budget) {
1345
1536
  server.registerTool(
1346
1537
  "blockrun_price",
1347
1538
  {
@@ -1372,18 +1563,38 @@ Examples:
1372
1563
  from: z10.number().optional().describe("History window start (unix seconds)."),
1373
1564
  to: z10.number().optional().describe("History window end (unix seconds)."),
1374
1565
  query: z10.string().optional().describe("Free-text filter for list."),
1375
- limit: z10.number().optional().describe("Max items for list (default 100, max 2000).")
1566
+ limit: z10.number().int().positive().max(2e3).optional().describe("Max items for list (default 100, max 2000)."),
1567
+ agent_id: z10.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1376
1568
  }
1377
1569
  },
1378
- async ({ action, category, symbol, market, session, resolution, from, to, query, limit }) => {
1570
+ async ({ action, category, symbol, market, session, resolution, from, to, query, limit, agent_id }) => {
1379
1571
  try {
1380
- const priceClient = getPriceClient();
1572
+ if (category === "stocks" && !market) {
1573
+ throw new Error("market is required when category='stocks'");
1574
+ }
1575
+ const paid = isPaidPriceCall(action, category);
1576
+ if (paid && getChain() !== "base") {
1577
+ return {
1578
+ content: [{ type: "text", text: formatError("Paid stock price/history calls currently settle on Base only. Switch BlockRun to Base (for example: write 'base' to ~/.blockrun/.chain) and fund the Base wallet with USDC.") }],
1579
+ isError: true
1580
+ };
1581
+ }
1582
+ const estimatedCost = paid ? 1e-3 : 0;
1583
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1584
+ if (!budgetCheck.allowed) {
1585
+ return {
1586
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1587
+ isError: true
1588
+ };
1589
+ }
1590
+ const priceClient = getPriceClient(paid);
1381
1591
  if (action === "price") {
1382
1592
  if (!symbol) throw new Error("symbol is required for action='price'");
1383
1593
  const result2 = await priceClient.price(category, symbol, {
1384
1594
  market,
1385
1595
  session
1386
1596
  });
1597
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
1387
1598
  return {
1388
1599
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
1389
1600
  structuredContent: result2
@@ -1391,7 +1602,7 @@ Examples:
1391
1602
  }
1392
1603
  if (action === "history") {
1393
1604
  if (!symbol) throw new Error("symbol is required for action='history'");
1394
- if (!from) throw new Error("from (unix seconds) is required for action='history'");
1605
+ if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
1395
1606
  const result2 = await priceClient.history(category, symbol, {
1396
1607
  market,
1397
1608
  session,
@@ -1399,6 +1610,7 @@ Examples:
1399
1610
  from,
1400
1611
  to
1401
1612
  });
1613
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
1402
1614
  return {
1403
1615
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
1404
1616
  structuredContent: result2
@@ -1507,7 +1719,10 @@ ${lines.join("\n\n")}` }],
1507
1719
 
1508
1720
  // src/tools/modal.ts
1509
1721
  import { z as z12 } from "zod";
1510
- function registerModalTool(server) {
1722
+ function estimateModalCost(path4) {
1723
+ return path4.includes("sandbox/create") ? 0.01 : 1e-3;
1724
+ }
1725
+ function registerModalTool(server, budget) {
1511
1726
  server.registerTool(
1512
1727
  "blockrun_modal",
1513
1728
  {
@@ -1524,15 +1739,25 @@ Common paths (all POST):
1524
1739
  Full action shapes + GPU type details in the \`modal\` skill.`,
1525
1740
  inputSchema: {
1526
1741
  path: z12.string().describe("Endpoint under /v1/modal/, e.g. 'sandbox/create', 'sandbox/exec'"),
1527
- body: z12.any().optional().describe("JSON body. Sent as POST.")
1742
+ body: z12.any().optional().describe("JSON body. Sent as POST."),
1743
+ agent_id: z12.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1528
1744
  }
1529
1745
  },
1530
- async ({ path: path4, body }) => {
1746
+ async ({ path: path4, body, agent_id }) => {
1531
1747
  try {
1532
- const client = getClient();
1533
1748
  const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
1749
+ const estimatedCost = estimateModalCost(cleanPath);
1750
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1751
+ if (!budgetCheck.allowed) {
1752
+ return {
1753
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1754
+ isError: true
1755
+ };
1756
+ }
1757
+ const client = getClient();
1534
1758
  const endpoint = `/v1/modal/${cleanPath}`;
1535
1759
  const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
1760
+ recordSpending(budget, estimatedCost, agent_id);
1536
1761
  return {
1537
1762
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1538
1763
  structuredContent: result
@@ -1546,7 +1771,17 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
1546
1771
 
1547
1772
  // src/tools/phone.ts
1548
1773
  import { z as z13 } from "zod";
1549
- function registerPhoneTool(server) {
1774
+ function estimatePhoneCost(path4, hasBody) {
1775
+ if (!hasBody && path4.startsWith("voice/call/")) return 0;
1776
+ if (path4 === "phone/numbers/release") return 0;
1777
+ if (path4 === "phone/lookup") return 0.01;
1778
+ if (path4 === "phone/lookup/fraud") return 0.05;
1779
+ if (path4 === "phone/numbers/buy" || path4 === "phone/numbers/renew") return 5;
1780
+ if (path4 === "phone/numbers/list") return 1e-3;
1781
+ if (path4 === "voice/call") return 0.54;
1782
+ return hasBody ? 1e-3 : 0;
1783
+ }
1784
+ function registerPhoneTool(server, budget) {
1550
1785
  server.registerTool(
1551
1786
  "blockrun_phone",
1552
1787
  {
@@ -1569,15 +1804,25 @@ Voice presets: nat, josh, maya, june, paige, derek, florian. Phone numbers use E
1569
1804
  Voice call flow + voice preset details + full body shapes in the \`phone\` skill.`,
1570
1805
  inputSchema: {
1571
1806
  path: z13.string().describe("Endpoint after /v1/. Use 'phone/...' for lookup + number ops, 'voice/call' for outbound AI calls, 'voice/call/{id}' (no body) to poll status."),
1572
- body: z13.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id}).")
1807
+ body: z13.any().optional().describe("JSON body. Sent as POST. Omit for the free GET poll (voice/call/{call_id})."),
1808
+ agent_id: z13.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1573
1809
  }
1574
1810
  },
1575
- async ({ path: path4, body }) => {
1811
+ async ({ path: path4, body, agent_id }) => {
1576
1812
  try {
1577
- const client = getClient();
1578
1813
  const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\//, "");
1814
+ const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
1815
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1816
+ if (!budgetCheck.allowed) {
1817
+ return {
1818
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1819
+ isError: true
1820
+ };
1821
+ }
1822
+ const client = getClient();
1579
1823
  const endpoint = `/v1/${cleanPath}`;
1580
1824
  const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
1825
+ if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
1581
1826
  return {
1582
1827
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1583
1828
  structuredContent: result
@@ -1591,7 +1836,44 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
1591
1836
 
1592
1837
  // src/tools/surf.ts
1593
1838
  import { z as z14 } from "zod";
1594
- function registerSurfTool(server) {
1839
+ var SURF_T3_PATHS = /* @__PURE__ */ new Set([
1840
+ "onchain/sql",
1841
+ "onchain/query",
1842
+ "onchain/schema",
1843
+ "chat/completions"
1844
+ ]);
1845
+ var SURF_T2_PATHS = /* @__PURE__ */ new Set([
1846
+ "exchange/depth",
1847
+ "exchange/klines",
1848
+ "exchange/funding-history",
1849
+ "exchange/long-short-ratio",
1850
+ "market/liquidation/exchange-list",
1851
+ "market/liquidation/order",
1852
+ "market/liquidation/chart",
1853
+ "market/onchain-indicator",
1854
+ "market/price-indicator",
1855
+ "prediction-market/polymarket/positions",
1856
+ "prediction-market/polymarket/activity",
1857
+ "social/detail",
1858
+ "social/ranking",
1859
+ "social/smart-followers/history",
1860
+ "social/mindshare",
1861
+ "token/dex-trades",
1862
+ "token/holders",
1863
+ "token/transfers",
1864
+ "web/fetch"
1865
+ ]);
1866
+ var SURF_T2_PREFIXES = ["search/", "wallet/"];
1867
+ function estimateSurfCost(path4) {
1868
+ const p = path4.toLowerCase().replace(/^\/+/, "");
1869
+ if (SURF_T3_PATHS.has(p)) return 0.02;
1870
+ if (SURF_T2_PATHS.has(p)) return 5e-3;
1871
+ for (const prefix of SURF_T2_PREFIXES) {
1872
+ if (p.startsWith(prefix)) return 5e-3;
1873
+ }
1874
+ return 1e-3;
1875
+ }
1876
+ function registerSurfTool(server, budget) {
1595
1877
  server.registerTool(
1596
1878
  "blockrun_surf",
1597
1879
  {
@@ -1619,15 +1901,25 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
1619
1901
  inputSchema: {
1620
1902
  path: z14.string().describe("Endpoint path under /v1/surf/, e.g. 'market/price', 'prediction-market/polymarket/ranking', 'wallet/detail', 'onchain/sql', 'chat/completions'"),
1621
1903
  params: z14.record(z14.string(), z14.string()).optional().describe("Query parameters for GET endpoints, e.g. { symbol: 'BTC' } or { address: '0x...', chain: 'ethereum' }"),
1622
- body: z14.any().optional().describe("JSON body for POST endpoints. Provide for: onchain/query, onchain/sql, chat/completions. When set, the call is sent as POST; otherwise GET with params.")
1904
+ body: z14.any().optional().describe("JSON body for POST endpoints. Provide for: onchain/query, onchain/sql, chat/completions. When set, the call is sent as POST; otherwise GET with params."),
1905
+ agent_id: z14.string().optional().describe("Agent identifier for budget tracking and enforcement.")
1623
1906
  }
1624
1907
  },
1625
- async ({ path: path4, params, body }) => {
1908
+ async ({ path: path4, params, body, agent_id }) => {
1626
1909
  try {
1627
- const client = getClient();
1628
1910
  const cleanPath = path4.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
1911
+ const estimatedCost = estimateSurfCost(cleanPath);
1912
+ const budgetCheck = checkBudget(budget, agent_id, estimatedCost);
1913
+ if (!budgetCheck.allowed) {
1914
+ return {
1915
+ content: [{ type: "text", text: `${budgetCheck.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
1916
+ isError: true
1917
+ };
1918
+ }
1919
+ const client = getClient();
1629
1920
  const endpoint = `/v1/surf/${cleanPath}`;
1630
1921
  const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
1922
+ recordSpending(budget, estimatedCost, agent_id);
1631
1923
  return {
1632
1924
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1633
1925
  structuredContent: result
@@ -1649,17 +1941,17 @@ function initializeMcpServer(server) {
1649
1941
  registerWalletTool(server, budget);
1650
1942
  registerChatTool(server, budget);
1651
1943
  registerModelsTool(server, modelCache);
1652
- registerImageTool(server);
1653
- registerMusicTool(server);
1654
- registerVideoTool(server);
1655
- registerSearchTool(server);
1656
- registerExaTool(server);
1657
- registerMarketsTool(server);
1658
- registerPriceTool(server);
1944
+ registerImageTool(server, budget);
1945
+ registerMusicTool(server, budget);
1946
+ registerVideoTool(server, budget);
1947
+ registerSearchTool(server, budget);
1948
+ registerExaTool(server, budget);
1949
+ registerMarketsTool(server, budget);
1950
+ registerPriceTool(server, budget);
1659
1951
  registerDexTool(server);
1660
- registerModalTool(server);
1661
- registerPhoneTool(server);
1662
- registerSurfTool(server);
1952
+ registerModalTool(server, budget);
1953
+ registerPhoneTool(server, budget);
1954
+ registerSurfTool(server, budget);
1663
1955
  server.registerResource(
1664
1956
  "wallet",
1665
1957
  "blockrun://wallet",
@@ -1682,7 +1974,7 @@ function initializeMcpServer(server) {
1682
1974
  async () => {
1683
1975
  const llm = getClient();
1684
1976
  if (!modelCache.models) {
1685
- modelCache.models = await llm.listModels();
1977
+ modelCache.models = "listAllModels" in llm ? await llm.listAllModels() : await llm.listModels();
1686
1978
  setTimeout(() => {
1687
1979
  modelCache.models = null;
1688
1980
  }, 5 * 60 * 1e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
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",