@blockrun/mcp 0.23.2 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +481 -375
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
|
|
8
8
|
// src/utils/wallet.ts
|
|
9
9
|
import fs from "fs";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import os2 from "os";
|
|
12
12
|
import {
|
|
13
13
|
LLMClient,
|
|
14
14
|
ImageClient,
|
|
@@ -23,10 +23,39 @@ import {
|
|
|
23
23
|
formatNeedsFundingMessage,
|
|
24
24
|
SOLANA_WALLET_FILE_PATH
|
|
25
25
|
} from "@blockrun/llm";
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// src/utils/constants.ts
|
|
28
|
+
import * as path from "path";
|
|
29
|
+
import * as os from "os";
|
|
30
|
+
var WALLET_DIR = path.join(os.homedir(), ".blockrun");
|
|
31
|
+
var WALLET_FILE = path.join(WALLET_DIR, ".session");
|
|
32
|
+
var QR_FILE = path.join(WALLET_DIR, "qr.png");
|
|
33
|
+
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
34
|
+
var BASE_CHAIN_ID = "8453";
|
|
35
|
+
var BASE_RPC_URLS = [
|
|
36
|
+
"https://mainnet.base.org",
|
|
37
|
+
"https://base.llamarpc.com",
|
|
38
|
+
"https://1rpc.io/base"
|
|
39
|
+
];
|
|
40
|
+
var MODEL_TIERS = {
|
|
41
|
+
fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
|
|
42
|
+
balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
|
|
43
|
+
powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
|
|
44
|
+
cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
|
|
45
|
+
reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
|
|
46
|
+
// 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
|
|
47
|
+
// (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
|
|
48
|
+
// anyway; these are the free models actually serving themselves.
|
|
49
|
+
free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
|
|
50
|
+
coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
|
|
51
|
+
glm: ["zai/glm-5", "zai/glm-5-turbo"]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/utils/wallet.ts
|
|
55
|
+
var BLOCKRUN_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
27
56
|
var CHAIN_PREFERENCE_FILES = [
|
|
28
|
-
|
|
29
|
-
|
|
57
|
+
path2.join(BLOCKRUN_DIR, ".chain"),
|
|
58
|
+
path2.join(BLOCKRUN_DIR, "payment-chain")
|
|
30
59
|
];
|
|
31
60
|
var _evmClient = null;
|
|
32
61
|
var _imageClient = null;
|
|
@@ -56,7 +85,7 @@ function getChain() {
|
|
|
56
85
|
}
|
|
57
86
|
return "base";
|
|
58
87
|
}
|
|
59
|
-
var CHAIN_FILE =
|
|
88
|
+
var CHAIN_FILE = path2.join(BLOCKRUN_DIR, ".chain");
|
|
60
89
|
function resetChainCaches() {
|
|
61
90
|
_evmClient = null;
|
|
62
91
|
_solanaClient = null;
|
|
@@ -185,17 +214,15 @@ async function getSolanaUsdcBalance() {
|
|
|
185
214
|
return null;
|
|
186
215
|
}
|
|
187
216
|
}
|
|
217
|
+
function parseBaseUsdcCallResult(raw) {
|
|
218
|
+
if (typeof raw !== "string" || !/^0x[0-9a-fA-F]+$/.test(raw)) return null;
|
|
219
|
+
return Number(BigInt(raw)) / 1e6;
|
|
220
|
+
}
|
|
188
221
|
async function getBaseUsdcBalance(address) {
|
|
189
|
-
const USDC_ADDRESS2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
190
|
-
const BASE_RPC_URLS = [
|
|
191
|
-
"https://mainnet.base.org",
|
|
192
|
-
"https://base.llamarpc.com",
|
|
193
|
-
"https://1rpc.io/base"
|
|
194
|
-
];
|
|
195
222
|
const data = {
|
|
196
223
|
jsonrpc: "2.0",
|
|
197
224
|
method: "eth_call",
|
|
198
|
-
params: [{ to:
|
|
225
|
+
params: [{ to: USDC_ADDRESS, data: `0x70a08231000000000000000000000000${address.slice(2)}` }, "latest"],
|
|
199
226
|
id: 1
|
|
200
227
|
};
|
|
201
228
|
for (const rpcUrl of BASE_RPC_URLS) {
|
|
@@ -206,7 +233,8 @@ async function getBaseUsdcBalance(address) {
|
|
|
206
233
|
body: JSON.stringify(data)
|
|
207
234
|
});
|
|
208
235
|
const result = await response.json();
|
|
209
|
-
|
|
236
|
+
const usd = parseBaseUsdcCallResult(result.result);
|
|
237
|
+
if (usd !== null) return usd;
|
|
210
238
|
} catch {
|
|
211
239
|
continue;
|
|
212
240
|
}
|
|
@@ -255,6 +283,25 @@ function checkBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
|
255
283
|
}
|
|
256
284
|
return { allowed: true };
|
|
257
285
|
}
|
|
286
|
+
function reserveBudget(budget, agentId, estimatedCost = 1e-3) {
|
|
287
|
+
const check = checkBudget(budget, agentId, estimatedCost);
|
|
288
|
+
if (!check.allowed) return { allowed: false, reason: check.reason, release: () => {
|
|
289
|
+
} };
|
|
290
|
+
const cost = Math.max(0, estimatedCost);
|
|
291
|
+
budget.spent += cost;
|
|
292
|
+
const agentBudget = agentId ? budget.agents.get(agentId) : void 0;
|
|
293
|
+
if (agentBudget) agentBudget.spent += cost;
|
|
294
|
+
let released = false;
|
|
295
|
+
return {
|
|
296
|
+
allowed: true,
|
|
297
|
+
release: () => {
|
|
298
|
+
if (released) return;
|
|
299
|
+
released = true;
|
|
300
|
+
budget.spent -= cost;
|
|
301
|
+
if (agentBudget) agentBudget.spent -= cost;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
258
305
|
function recordSpending(budget, cost, agentId) {
|
|
259
306
|
budget.spent += cost;
|
|
260
307
|
budget.calls += 1;
|
|
@@ -289,30 +336,6 @@ import QRCode from "qrcode";
|
|
|
289
336
|
import open from "open";
|
|
290
337
|
import * as fs2 from "fs";
|
|
291
338
|
import * as path3 from "path";
|
|
292
|
-
|
|
293
|
-
// src/utils/constants.ts
|
|
294
|
-
import * as path2 from "path";
|
|
295
|
-
import * as os2 from "os";
|
|
296
|
-
var WALLET_DIR = path2.join(os2.homedir(), ".blockrun");
|
|
297
|
-
var WALLET_FILE = path2.join(WALLET_DIR, ".session");
|
|
298
|
-
var QR_FILE = path2.join(WALLET_DIR, "qr.png");
|
|
299
|
-
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
300
|
-
var BASE_CHAIN_ID = "8453";
|
|
301
|
-
var MODEL_TIERS = {
|
|
302
|
-
fast: ["google/gemini-3.5-flash", "google/gemini-2.5-flash", "google/gemini-3.1-flash-lite", "openai/gpt-5-mini", "deepseek/deepseek-chat", "google/gemini-3-flash-preview"],
|
|
303
|
-
balanced: ["openai/gpt-5.5", "anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro", "moonshot/kimi-k2.6", "openai/gpt-5.3", "openai/gpt-5.4"],
|
|
304
|
-
powerful: ["anthropic/claude-opus-4.8", "openai/gpt-5.4-pro", "anthropic/claude-opus-4.7", "anthropic/claude-opus-4.6", "openai/o3", "openai/gpt-5.4"],
|
|
305
|
-
cheap: ["zai/glm-5", "zai/glm-5-turbo", "nvidia/gpt-oss-120b", "nvidia/deepseek-v4-flash", "google/gemini-2.5-flash", "deepseek/deepseek-chat", "openai/gpt-5.4-nano"],
|
|
306
|
-
reasoning: ["anthropic/claude-opus-4.8", "openai/o3", "openai/o1", "openai/o3-mini", "deepseek/deepseek-reasoner", "moonshot/kimi-k2.6", "openai/gpt-5.3-codex"],
|
|
307
|
-
// 2026-06-07 sweep: dropped qwen3-next (NVIDIA EOL, 410), mistral-small-4-119b
|
|
308
|
-
// (timing out), deepseek-v3.2 + glm-4.7 (NIM hung). All redirect server-side
|
|
309
|
-
// anyway; these are the free models actually serving themselves.
|
|
310
|
-
free: ["nvidia/llama-4-maverick", "nvidia/qwen3-coder-480b", "nvidia/deepseek-v4-flash", "nvidia/gpt-oss-120b", "nvidia/gpt-oss-20b"],
|
|
311
|
-
coding: ["anthropic/claude-opus-4.8", "zai/glm-5", "openai/gpt-5.3-codex", "moonshot/kimi-k2.6", "nvidia/qwen3-coder-480b", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4"],
|
|
312
|
-
glm: ["zai/glm-5", "zai/glm-5-turbo"]
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
// src/utils/qr.ts
|
|
316
339
|
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
317
340
|
var sharpModule;
|
|
318
341
|
async function loadSharp() {
|
|
@@ -423,9 +446,13 @@ ${parts.join("\n")}`;
|
|
|
423
446
|
}
|
|
424
447
|
return base;
|
|
425
448
|
}
|
|
449
|
+
function isPaymentRejectionError(message) {
|
|
450
|
+
const m = message.toLowerCase();
|
|
451
|
+
return m.includes("insufficient") || m.includes("balance") || m.includes("rejected");
|
|
452
|
+
}
|
|
426
453
|
function formatError(message, opts) {
|
|
427
454
|
const msgLower = message.toLowerCase();
|
|
428
|
-
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}([^0-9]
|
|
455
|
+
const hasStatus = (code) => new RegExp(`(^|[^0-9.])${code}($|[^0-9.])`).test(msgLower);
|
|
429
456
|
const isPaymentError = hasStatus("402") || msgLower.includes("balance") || msgLower.includes("insufficient") || msgLower.includes("payment") && !hasStatus("500");
|
|
430
457
|
const isModelUnavailable = msgLower.includes("not active for requested provider") || msgLower.includes("not found or not active");
|
|
431
458
|
const isServerError = hasStatus("500") || msgLower.includes("api error after payment");
|
|
@@ -814,6 +841,7 @@ async function handleAnthropicNative(args) {
|
|
|
814
841
|
temperature,
|
|
815
842
|
stop,
|
|
816
843
|
thinking,
|
|
844
|
+
responseFormat,
|
|
817
845
|
budget,
|
|
818
846
|
agentId,
|
|
819
847
|
estimatedCost
|
|
@@ -829,6 +857,9 @@ async function handleAnthropicNative(args) {
|
|
|
829
857
|
}
|
|
830
858
|
apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
|
|
831
859
|
}
|
|
860
|
+
if (responseFormat?.type === "json_object") {
|
|
861
|
+
systemParts.push("Respond with only valid JSON. Do not wrap it in markdown code fences or add any prose before or after.");
|
|
862
|
+
}
|
|
832
863
|
if (message.trim()) apiMessages.push({ role: "user", content: message });
|
|
833
864
|
if (apiMessages.length === 0) {
|
|
834
865
|
return { content: [{ type: "text", text: "No message content to send." }], isError: true };
|
|
@@ -898,7 +929,6 @@ ${thinkingText}` });
|
|
|
898
929
|
function estimateChatCost(maxTokens, mode, model, routing, routingProfile) {
|
|
899
930
|
if (mode === "free") return 0;
|
|
900
931
|
if (model?.startsWith("nvidia/")) return 0;
|
|
901
|
-
if (routing === "smart" && routingProfile === "free") return 0;
|
|
902
932
|
const out = Math.max(maxTokens ?? 1024, 256);
|
|
903
933
|
const frontierReserve = Math.max(0.01, out / 1e6 * 20);
|
|
904
934
|
if (routing === "smart") {
|
|
@@ -944,7 +974,7 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
944
974
|
model: z2.string().optional().describe("Specific model ID (e.g., 'zai/glm-5', 'openai/o3')"),
|
|
945
975
|
mode: z2.enum(["fast", "balanced", "powerful", "cheap", "reasoning", "free", "coding", "glm"]).optional().describe("Routing mode: glm = Zhipu GLM-5/GLM-5-Turbo ($0.001/call, great for coding), coding = GLM-5 + code models, cheap = GLM-5 + budget, free = NVIDIA only (ignored if model specified)"),
|
|
946
976
|
routing: z2.enum(["smart"]).optional().describe('Set to "smart" to auto-select the optimal model via ClawRouter (14-dimension AI routing)'),
|
|
947
|
-
routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "
|
|
977
|
+
routing_profile: z2.enum(["free", "eco", "auto", "premium"]).optional().default("auto").describe('Cost/quality profile for ClawRouter: "eco" (budget), "auto" (balanced, default), "premium" (best quality). Note: "free" maps to "auto" (the SDK dropped the free profile) and still settles a PAID model \u2014 for zero-cost generation use mode:"free" or model:"nvidia/...". Only applies when routing:"smart".'),
|
|
948
978
|
system: z2.string().optional().describe("Optional system prompt"),
|
|
949
979
|
max_tokens: z2.number().optional().default(1024).describe("Max tokens in response"),
|
|
950
980
|
temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
|
|
@@ -971,141 +1001,152 @@ Run blockrun_models to see all available models with pricing.`,
|
|
|
971
1001
|
const llm = getClient();
|
|
972
1002
|
const responseFormat = response_format ? { type: response_format } : void 0;
|
|
973
1003
|
const estimatedCost = estimateChatCost(max_tokens, mode, model, routing, routing_profile);
|
|
974
|
-
const
|
|
975
|
-
if (!
|
|
1004
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1005
|
+
if (!gate.allowed) {
|
|
976
1006
|
return {
|
|
977
|
-
content: [{ type: "text", text: `${
|
|
1007
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet with action: "report" to see usage, or action: "delegate" to increase agent budget.` }],
|
|
978
1008
|
isError: true
|
|
979
1009
|
};
|
|
980
1010
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
messages,
|
|
992
|
-
maxTokens: max_tokens,
|
|
993
|
-
temperature,
|
|
994
|
-
stop,
|
|
995
|
-
thinking,
|
|
996
|
-
budget,
|
|
997
|
-
agentId: agent_id,
|
|
998
|
-
estimatedCost
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
if (routing === "smart") {
|
|
1002
|
-
if (!(llm instanceof LLMClient2)) {
|
|
1003
|
-
return {
|
|
1004
|
-
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
1005
|
-
isError: true
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
try {
|
|
1009
|
-
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1011
|
+
try {
|
|
1012
|
+
if (model && isAnthropicModel(model)) {
|
|
1013
|
+
const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
|
|
1014
|
+
if (solanaBlock) {
|
|
1015
|
+
return { content: [{ type: "text", text: solanaBlock }], isError: true };
|
|
1016
|
+
}
|
|
1017
|
+
return await handleAnthropicNative({
|
|
1018
|
+
client: getAnthropicClient(),
|
|
1019
|
+
model,
|
|
1020
|
+
message,
|
|
1010
1021
|
system,
|
|
1022
|
+
messages,
|
|
1011
1023
|
maxTokens: max_tokens,
|
|
1012
|
-
maxOutputTokens: max_tokens,
|
|
1013
1024
|
temperature,
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1017
|
-
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1025
|
+
stop,
|
|
1026
|
+
thinking,
|
|
1018
1027
|
responseFormat,
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1028
|
+
budget,
|
|
1029
|
+
agentId: agent_id,
|
|
1030
|
+
estimatedCost
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
if (routing === "smart") {
|
|
1034
|
+
if (messages && messages.length > 0) {
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{ type: "text", text: formatError('routing:"smart" does not support multi-turn `messages` \u2014 smart routing answers a single prompt. Send the conversation via `messages` with an explicit `model`/`mode` (no routing), or send a single `message` with routing:"smart".') }],
|
|
1037
|
+
isError: true
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (!(llm instanceof LLMClient2)) {
|
|
1041
|
+
return {
|
|
1042
|
+
content: [{ type: "text", text: "Smart routing (ClawRouter) is not available on Solana. Use a specific model or mode instead." }],
|
|
1043
|
+
isError: true
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.smartChat(message, {
|
|
1048
|
+
system,
|
|
1049
|
+
maxTokens: max_tokens,
|
|
1050
|
+
maxOutputTokens: max_tokens,
|
|
1051
|
+
temperature,
|
|
1052
|
+
// @blockrun/llm 2.x dropped the "free" routing profile; the gateway
|
|
1053
|
+
// already routes to the most cost-effective model by default, so we
|
|
1054
|
+
// omit it and let ClawRouter pick (matches the SDK upgrade path).
|
|
1055
|
+
routingProfile: routing_profile === "free" ? void 0 : routing_profile,
|
|
1056
|
+
responseFormat,
|
|
1057
|
+
stop
|
|
1058
|
+
}));
|
|
1059
|
+
recordActualSpend(budget, settledUsd, result.routing.costEstimate || estimatedCost, agent_id);
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{ type: "text", text: `[${result.model} | ${result.routing.tier} | $${result.routing.costEstimate.toFixed(4)} | ${Math.round((result.routing.savings ?? 0) * 100)}% savings]
|
|
1024
1062
|
|
|
1025
1063
|
${result.response}` }],
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1064
|
+
structuredContent: {
|
|
1065
|
+
model_used: result.model,
|
|
1066
|
+
response: result.response,
|
|
1067
|
+
routing: result.routing
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1072
|
+
}
|
|
1034
1073
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1074
|
+
if (messages && messages.length > 0) {
|
|
1075
|
+
const targetModel = model || MODEL_TIERS[mode ?? "balanced"]?.[0] || "openai/gpt-5.5";
|
|
1076
|
+
const fullMessages = [
|
|
1077
|
+
...system ? [{ role: "system", content: system }] : [],
|
|
1078
|
+
...messages,
|
|
1079
|
+
{ role: "user", content: message }
|
|
1080
|
+
];
|
|
1081
|
+
try {
|
|
1082
|
+
const { result, settledUsd } = await withSettledCost(llm, () => llm.chatCompletion(targetModel, fullMessages, {
|
|
1083
|
+
maxTokens: max_tokens,
|
|
1084
|
+
temperature,
|
|
1085
|
+
responseFormat,
|
|
1086
|
+
stop
|
|
1087
|
+
}));
|
|
1088
|
+
const reply = result.choices?.[0]?.message?.content || "";
|
|
1089
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{ type: "text", text: `[${targetModel} | ${fullMessages.length} msgs]
|
|
1054
1092
|
|
|
1055
1093
|
${reply}` }],
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1094
|
+
structuredContent: { model_used: targetModel, response: reply, message_count: fullMessages.length }
|
|
1095
|
+
};
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
return { content: [{ type: "text", text: formatError(extractErrorMessage(error)) }], isError: true };
|
|
1098
|
+
}
|
|
1060
1099
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1100
|
+
if (model) {
|
|
1101
|
+
try {
|
|
1102
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(model, message, {
|
|
1103
|
+
system,
|
|
1104
|
+
maxTokens: max_tokens,
|
|
1105
|
+
temperature,
|
|
1106
|
+
responseFormat,
|
|
1107
|
+
stop
|
|
1108
|
+
}));
|
|
1109
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1110
|
+
return { content: [{ type: "text", text: response }] };
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return {
|
|
1113
|
+
content: [{ type: "text", text: formatError(extractErrorMessage(error)) }],
|
|
1114
|
+
isError: true
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1078
1117
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
content: [{ type: "text", text: `[${m}]
|
|
1118
|
+
const routingMode = mode || "balanced";
|
|
1119
|
+
const models = MODEL_TIERS[routingMode];
|
|
1120
|
+
let lastError = null;
|
|
1121
|
+
for (const m of models) {
|
|
1122
|
+
try {
|
|
1123
|
+
const { result: response, settledUsd } = await withSettledCost(llm, () => llm.chat(m, message, {
|
|
1124
|
+
system,
|
|
1125
|
+
maxTokens: max_tokens,
|
|
1126
|
+
temperature,
|
|
1127
|
+
responseFormat,
|
|
1128
|
+
stop
|
|
1129
|
+
}));
|
|
1130
|
+
recordActualSpend(budget, settledUsd, estimatedCost, agent_id);
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{ type: "text", text: `[${m}]
|
|
1095
1133
|
|
|
1096
1134
|
${response}` }],
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1135
|
+
structuredContent: { model_used: m, response }
|
|
1136
|
+
};
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
lastError = error;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1102
1141
|
}
|
|
1142
|
+
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1143
|
+
return {
|
|
1144
|
+
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1145
|
+
isError: true
|
|
1146
|
+
};
|
|
1147
|
+
} finally {
|
|
1148
|
+
gate.release();
|
|
1103
1149
|
}
|
|
1104
|
-
const errorMessage = lastError ? extractErrorMessage(lastError) : "All models failed";
|
|
1105
|
-
return {
|
|
1106
|
-
content: [{ type: "text", text: formatError(errorMessage) }],
|
|
1107
|
-
isError: true
|
|
1108
|
-
};
|
|
1109
1150
|
}
|
|
1110
1151
|
);
|
|
1111
1152
|
}
|
|
@@ -1193,6 +1234,10 @@ async function toImageDataUri(ref) {
|
|
|
1193
1234
|
if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
|
|
1194
1235
|
const mime2 = (res.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
|
|
1195
1236
|
if (!mime2.startsWith("image/")) throw new Error(`URL returned non-image content-type: ${mime2 || "(none)"}`);
|
|
1237
|
+
const advertised = Number(res.headers.get("content-length"));
|
|
1238
|
+
if (Number.isFinite(advertised) && advertised > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1239
|
+
throw new Error(`image too large: ${(advertised / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
1240
|
+
}
|
|
1196
1241
|
const buffer2 = Buffer.from(await res.arrayBuffer());
|
|
1197
1242
|
if (buffer2.byteLength > REFERENCE_IMAGE_MAX_BYTES) {
|
|
1198
1243
|
throw new Error(`image too large: ${(buffer2.byteLength / 1e6).toFixed(1)}MB > ${REFERENCE_IMAGE_MAX_BYTES / 1e6}MB cap`);
|
|
@@ -1349,34 +1394,42 @@ Source images and masks accept a base64 data URI, an http(s) URL, or a local fil
|
|
|
1349
1394
|
};
|
|
1350
1395
|
}
|
|
1351
1396
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1352
|
-
const
|
|
1353
|
-
if (!
|
|
1397
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1398
|
+
if (!gate.allowed) {
|
|
1354
1399
|
return {
|
|
1355
|
-
content: [{ type: "text", text: `${
|
|
1400
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1356
1401
|
isError: true
|
|
1357
1402
|
};
|
|
1358
1403
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1404
|
+
try {
|
|
1405
|
+
response = await getImageClient().edit(prompt, normalizedImage, {
|
|
1406
|
+
model: selectedModel,
|
|
1407
|
+
size,
|
|
1408
|
+
...normalizedMask ? { mask: normalizedMask } : {}
|
|
1409
|
+
});
|
|
1410
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1411
|
+
} finally {
|
|
1412
|
+
gate.release();
|
|
1413
|
+
}
|
|
1365
1414
|
} else {
|
|
1366
1415
|
const estimatedCost = estimateCost(selectedModel, size);
|
|
1367
|
-
const
|
|
1368
|
-
if (!
|
|
1416
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1417
|
+
if (!gate.allowed) {
|
|
1369
1418
|
return {
|
|
1370
|
-
content: [{ type: "text", text: `${
|
|
1419
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1371
1420
|
isError: true
|
|
1372
1421
|
};
|
|
1373
1422
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1423
|
+
try {
|
|
1424
|
+
response = await getImageClient().generate(prompt, {
|
|
1425
|
+
model: selectedModel,
|
|
1426
|
+
size,
|
|
1427
|
+
quality
|
|
1428
|
+
});
|
|
1429
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
1430
|
+
} finally {
|
|
1431
|
+
gate.release();
|
|
1432
|
+
}
|
|
1380
1433
|
}
|
|
1381
1434
|
const imageUrl = response.data?.[0]?.url;
|
|
1382
1435
|
if (!imageUrl) {
|
|
@@ -1417,11 +1470,7 @@ async function fetchWithTimeout(url, options, timeoutMs) {
|
|
|
1417
1470
|
const controller = new AbortController();
|
|
1418
1471
|
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
1419
1472
|
id.unref?.();
|
|
1420
|
-
|
|
1421
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
1422
|
-
} finally {
|
|
1423
|
-
clearTimeout(id);
|
|
1424
|
-
}
|
|
1473
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
1425
1474
|
}
|
|
1426
1475
|
function isTimeoutError(err) {
|
|
1427
1476
|
const name = err instanceof Error ? err.name : "";
|
|
@@ -1460,6 +1509,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1460
1509
|
}
|
|
1461
1510
|
},
|
|
1462
1511
|
async ({ prompt, instrumental, lyrics, model, agent_id }) => {
|
|
1512
|
+
let gate;
|
|
1463
1513
|
try {
|
|
1464
1514
|
if (getChain() !== "base") {
|
|
1465
1515
|
return {
|
|
@@ -1473,10 +1523,10 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1473
1523
|
isError: true
|
|
1474
1524
|
};
|
|
1475
1525
|
}
|
|
1476
|
-
|
|
1477
|
-
if (!
|
|
1526
|
+
gate = reserveBudget(budget, agent_id, MUSIC_COST);
|
|
1527
|
+
if (!gate.allowed) {
|
|
1478
1528
|
return {
|
|
1479
|
-
content: [{ type: "text", text: `${
|
|
1529
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1480
1530
|
isError: true
|
|
1481
1531
|
};
|
|
1482
1532
|
}
|
|
@@ -1491,8 +1541,8 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1491
1541
|
body: JSON.stringify(body)
|
|
1492
1542
|
}, 15e3);
|
|
1493
1543
|
if (resp402.status !== 402) {
|
|
1494
|
-
const data2 = await resp402.json();
|
|
1495
|
-
throw new Error(`
|
|
1544
|
+
const data2 = await resp402.json().catch(() => ({}));
|
|
1545
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1496
1546
|
}
|
|
1497
1547
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1498
1548
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1553,7 +1603,7 @@ Returns a time-limited CDN URL \u2014 download immediately if you need to keep t
|
|
|
1553
1603
|
};
|
|
1554
1604
|
} catch (err) {
|
|
1555
1605
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1556
|
-
if (
|
|
1606
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1557
1607
|
return {
|
|
1558
1608
|
content: [{ type: "text", text: `Music generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1559
1609
|
Error: ${errMsg}` }],
|
|
@@ -1571,6 +1621,8 @@ Error: ${errMsg}` }],
|
|
|
1571
1621
|
content: [{ type: "text", text: formatError(`Music generation failed: ${errMsg}`) }],
|
|
1572
1622
|
isError: true
|
|
1573
1623
|
};
|
|
1624
|
+
} finally {
|
|
1625
|
+
gate?.release();
|
|
1574
1626
|
}
|
|
1575
1627
|
}
|
|
1576
1628
|
);
|
|
@@ -1640,6 +1692,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1640
1692
|
}
|
|
1641
1693
|
},
|
|
1642
1694
|
async ({ action, input, voice, model, response_format, speed, duration_seconds, prompt_influence, agent_id }) => {
|
|
1695
|
+
let gate;
|
|
1643
1696
|
try {
|
|
1644
1697
|
if (action === "voices") {
|
|
1645
1698
|
return await listVoices();
|
|
@@ -1684,10 +1737,10 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1684
1737
|
if (speed !== void 0) body.speed = speed;
|
|
1685
1738
|
cost = speechCost(model, input.length);
|
|
1686
1739
|
}
|
|
1687
|
-
|
|
1688
|
-
if (!
|
|
1740
|
+
gate = reserveBudget(budget, agent_id, cost);
|
|
1741
|
+
if (!gate.allowed) {
|
|
1689
1742
|
return {
|
|
1690
|
-
content: [{ type: "text", text: `${
|
|
1743
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1691
1744
|
isError: true
|
|
1692
1745
|
};
|
|
1693
1746
|
}
|
|
@@ -1700,7 +1753,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1700
1753
|
}, 15e3);
|
|
1701
1754
|
if (resp402.status !== 402) {
|
|
1702
1755
|
const data2 = await resp402.json().catch(() => ({}));
|
|
1703
|
-
throw new Error(`
|
|
1756
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data2)}`);
|
|
1704
1757
|
}
|
|
1705
1758
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1706
1759
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -1766,7 +1819,7 @@ Returns a hosted audio URL \u2014 download immediately if you need to keep the f
|
|
|
1766
1819
|
};
|
|
1767
1820
|
} catch (err) {
|
|
1768
1821
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1769
|
-
if (
|
|
1822
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
1770
1823
|
return {
|
|
1771
1824
|
content: [{ type: "text", text: `Speech generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
1772
1825
|
Error: ${errMsg}` }],
|
|
@@ -1784,6 +1837,8 @@ Error: ${errMsg}` }],
|
|
|
1784
1837
|
content: [{ type: "text", text: formatError(`Speech generation failed: ${errMsg}`) }],
|
|
1785
1838
|
isError: true
|
|
1786
1839
|
};
|
|
1840
|
+
} finally {
|
|
1841
|
+
gate?.release();
|
|
1787
1842
|
}
|
|
1788
1843
|
}
|
|
1789
1844
|
);
|
|
@@ -1884,6 +1939,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1884
1939
|
}
|
|
1885
1940
|
},
|
|
1886
1941
|
async ({ prompt, image_url, real_face_asset_id, duration_seconds, generate_audio, resolution, aspect_ratio, last_frame_url, model, agent_id }) => {
|
|
1942
|
+
let gate;
|
|
1887
1943
|
try {
|
|
1888
1944
|
if (getChain() !== "base") {
|
|
1889
1945
|
return {
|
|
@@ -1924,10 +1980,10 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1924
1980
|
const hasImageInput = Boolean(image_url || real_face_asset_id);
|
|
1925
1981
|
const perSecond = (hasImageInput ? VIDEO_PRICE_PER_SECOND_IMAGE[selectedModel] : void 0) ?? VIDEO_PRICE_PER_SECOND[selectedModel] ?? 0.05;
|
|
1926
1982
|
const estimatedCost = perSecond * billedSeconds;
|
|
1927
|
-
|
|
1928
|
-
if (!
|
|
1983
|
+
gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
1984
|
+
if (!gate.allowed) {
|
|
1929
1985
|
return {
|
|
1930
|
-
content: [{ type: "text", text: `${
|
|
1986
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
1931
1987
|
isError: true
|
|
1932
1988
|
};
|
|
1933
1989
|
}
|
|
@@ -1948,8 +2004,8 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
1948
2004
|
body: JSON.stringify(body)
|
|
1949
2005
|
}, 15e3);
|
|
1950
2006
|
if (resp402.status !== 402) {
|
|
1951
|
-
const data = await resp402.json();
|
|
1952
|
-
throw new Error(`
|
|
2007
|
+
const data = await resp402.json().catch(() => ({}));
|
|
2008
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${JSON.stringify(data)}`);
|
|
1953
2009
|
}
|
|
1954
2010
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
1955
2011
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -2053,7 +2109,7 @@ Returns a permanent blockrun-hosted MP4 URL (the gateway mirrors the asset to GC
|
|
|
2053
2109
|
};
|
|
2054
2110
|
} catch (err) {
|
|
2055
2111
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2056
|
-
if (
|
|
2112
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2057
2113
|
return {
|
|
2058
2114
|
content: [{ type: "text", text: `Video generation requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2059
2115
|
Error: ${errMsg}` }],
|
|
@@ -2071,6 +2127,8 @@ Error: ${errMsg}` }],
|
|
|
2071
2127
|
content: [{ type: "text", text: formatError(`Video generation failed: ${errMsg}`, { altModels: "bytedance/seedance-2.0, azure/sora-2" }) }],
|
|
2072
2128
|
isError: true
|
|
2073
2129
|
};
|
|
2130
|
+
} finally {
|
|
2131
|
+
gate?.release();
|
|
2074
2132
|
}
|
|
2075
2133
|
}
|
|
2076
2134
|
);
|
|
@@ -2096,7 +2154,7 @@ async function payAndPostJson(url, reqBody, fallbackDescription) {
|
|
|
2096
2154
|
}, 15e3);
|
|
2097
2155
|
if (resp402.status !== 402) {
|
|
2098
2156
|
const data2 = await resp402.json().catch(() => ({}));
|
|
2099
|
-
throw new Error(`
|
|
2157
|
+
throw new Error(`Unexpected status ${resp402.status} (the endpoint did not return a quote): ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2100
2158
|
}
|
|
2101
2159
|
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2102
2160
|
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
@@ -2157,6 +2215,7 @@ Privacy: BlockRun does not store face/liveness data \u2014 only the asset id, na
|
|
|
2157
2215
|
}
|
|
2158
2216
|
},
|
|
2159
2217
|
async ({ action, name, group_id, image_url, agent_id }) => {
|
|
2218
|
+
let gate;
|
|
2160
2219
|
try {
|
|
2161
2220
|
if (action === "init") {
|
|
2162
2221
|
if (!name) {
|
|
@@ -2283,9 +2342,9 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2283
2342
|
if (!name || !image_url) {
|
|
2284
2343
|
return { content: [{ type: "text", text: formatError("portrait requires name and image_url (public HTTPS URL of an AI-generated character image).") }], isError: true };
|
|
2285
2344
|
}
|
|
2286
|
-
|
|
2287
|
-
if (!
|
|
2288
|
-
return { content: [{ type: "text", text: `${
|
|
2345
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2346
|
+
if (!gate.allowed) {
|
|
2347
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2289
2348
|
}
|
|
2290
2349
|
const { status, data, settledUsd } = await payAndPostJson(
|
|
2291
2350
|
`${BLOCKRUN_API4}/v1/portrait/enroll`,
|
|
@@ -2333,61 +2392,26 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2333
2392
|
if (!name || !image_url || !group_id) {
|
|
2334
2393
|
return { content: [{ type: "text", text: formatError("enroll requires name, image_url, and group_id (from init, after the group is active).") }], isError: true };
|
|
2335
2394
|
}
|
|
2336
|
-
|
|
2337
|
-
if (!
|
|
2338
|
-
return { content: [{ type: "text", text: `${
|
|
2395
|
+
gate = reserveBudget(budget, agent_id, ENROLLMENT_PRICE_USD);
|
|
2396
|
+
if (!gate.allowed) {
|
|
2397
|
+
return { content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }], isError: true };
|
|
2339
2398
|
}
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
const resp402 = await fetchWithTimeout(enrollUrl, {
|
|
2345
|
-
method: "POST",
|
|
2346
|
-
headers: { "Content-Type": "application/json" },
|
|
2347
|
-
body: reqBody
|
|
2348
|
-
}, 15e3);
|
|
2349
|
-
if (resp402.status !== 402) {
|
|
2350
|
-
const data2 = await resp402.json().catch(() => ({}));
|
|
2351
|
-
throw new Error(`Expected 402, got ${resp402.status}: ${data2.message || data2.error || JSON.stringify(data2)}`);
|
|
2352
|
-
}
|
|
2353
|
-
const prHeader = resp402.headers.get("payment-required") || resp402.headers.get("PAYMENT-REQUIRED");
|
|
2354
|
-
if (!prHeader) throw new Error("No PAYMENT-REQUIRED header in 402 response");
|
|
2355
|
-
const paymentRequired = parsePaymentRequired4(prHeader);
|
|
2356
|
-
const details = extractPaymentDetails4(paymentRequired);
|
|
2357
|
-
const settledUsd = amountToUsd(details.amount);
|
|
2358
|
-
const paymentPayload = await createPaymentPayload4(
|
|
2359
|
-
privateKey,
|
|
2360
|
-
account.address,
|
|
2361
|
-
details.recipient,
|
|
2362
|
-
details.amount,
|
|
2363
|
-
details.network || "eip155:8453",
|
|
2364
|
-
{
|
|
2365
|
-
resourceUrl: details.resource?.url || enrollUrl,
|
|
2366
|
-
resourceDescription: details.resource?.description || "BlockRun RealFace enrollment",
|
|
2367
|
-
maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, 120),
|
|
2368
|
-
extra: details.extra
|
|
2369
|
-
}
|
|
2399
|
+
const { status, data, settledUsd } = await payAndPostJson(
|
|
2400
|
+
`${BLOCKRUN_API4}/v1/realface/enroll`,
|
|
2401
|
+
JSON.stringify({ name, image_url, group_id }),
|
|
2402
|
+
"BlockRun RealFace enrollment"
|
|
2370
2403
|
);
|
|
2371
|
-
|
|
2372
|
-
method: "POST",
|
|
2373
|
-
headers: {
|
|
2374
|
-
"Content-Type": "application/json",
|
|
2375
|
-
"PAYMENT-SIGNATURE": paymentPayload
|
|
2376
|
-
},
|
|
2377
|
-
body: reqBody
|
|
2378
|
-
}, 9e4);
|
|
2379
|
-
const data = await resp.json().catch(() => ({}));
|
|
2380
|
-
if (resp.status === 402) {
|
|
2404
|
+
if (status === 402) {
|
|
2381
2405
|
throw new Error("Payment rejected. Check your wallet balance.");
|
|
2382
2406
|
}
|
|
2383
|
-
if (
|
|
2407
|
+
if (status === 425) {
|
|
2384
2408
|
return { content: [{ type: "text", text: formatError(`Group not active yet \u2014 ${data.message || "finish the phone liveness check first"}. No payment taken.`) }], isError: true };
|
|
2385
2409
|
}
|
|
2386
|
-
if (
|
|
2410
|
+
if (status === 422) {
|
|
2387
2411
|
return { content: [{ type: "text", text: formatError(`Face match failed \u2014 ${data.hint || "use a clearer front-facing photo of the same person"}. No payment taken.`) }], isError: true };
|
|
2388
2412
|
}
|
|
2389
|
-
if (
|
|
2390
|
-
throw new Error(`Enroll error ${
|
|
2413
|
+
if (status < 200 || status >= 300) {
|
|
2414
|
+
throw new Error(`Enroll error ${status}: ${data.error || JSON.stringify(data)}`);
|
|
2391
2415
|
}
|
|
2392
2416
|
const assetId = data.asset_id;
|
|
2393
2417
|
if (!assetId) throw new Error(`Enroll response missing asset_id: ${JSON.stringify(data)}`);
|
|
@@ -2416,7 +2440,7 @@ Enroll one: blockrun_realface action:"init" name:"\u2026" (real person) or actio
|
|
|
2416
2440
|
return { content: [{ type: "text", text: formatError(`Unknown action: ${action}`) }], isError: true };
|
|
2417
2441
|
} catch (err) {
|
|
2418
2442
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2419
|
-
if (
|
|
2443
|
+
if (isPaymentRejectionError(errMsg)) {
|
|
2420
2444
|
return {
|
|
2421
2445
|
content: [{ type: "text", text: `RealFace enrollment requires payment. Run blockrun_wallet with action: "setup" for funding instructions.
|
|
2422
2446
|
Error: ${errMsg}` }],
|
|
@@ -2424,6 +2448,8 @@ Error: ${errMsg}` }],
|
|
|
2424
2448
|
};
|
|
2425
2449
|
}
|
|
2426
2450
|
return { content: [{ type: "text", text: formatError(`RealFace ${action} failed: ${errMsg}`) }], isError: true };
|
|
2451
|
+
} finally {
|
|
2452
|
+
gate?.release();
|
|
2427
2453
|
}
|
|
2428
2454
|
}
|
|
2429
2455
|
);
|
|
@@ -2447,6 +2473,19 @@ function asStructuredContent(result) {
|
|
|
2447
2473
|
return typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result };
|
|
2448
2474
|
}
|
|
2449
2475
|
|
|
2476
|
+
// src/utils/path-safety.ts
|
|
2477
|
+
function hasPathTraversal(path5) {
|
|
2478
|
+
let decoded = path5;
|
|
2479
|
+
try {
|
|
2480
|
+
decoded = decodeURIComponent(path5);
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
return decoded.split(/[/\\]/).some((seg) => seg === ".." || seg === ".");
|
|
2484
|
+
}
|
|
2485
|
+
function isValidNetworkSlug(slug) {
|
|
2486
|
+
return /^[a-z0-9-]+$/.test(slug);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2450
2489
|
// src/tools/search.ts
|
|
2451
2490
|
var SEARCH_PRICE_PER_SOURCE = 0.025;
|
|
2452
2491
|
var SEARCH_DEFAULT_MAX_RESULTS = 10;
|
|
@@ -2477,23 +2516,30 @@ Full request shape + worked examples in the \`search\` skill (\`skills/search/SK
|
|
|
2477
2516
|
async ({ path: path5, body, agent_id }) => {
|
|
2478
2517
|
try {
|
|
2479
2518
|
body = coerceBody(body);
|
|
2519
|
+
const cleanPath = (path5 ?? "").replace(/^\/+/, "").replace(/^v1\/search\/?/, "");
|
|
2520
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2521
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2522
|
+
}
|
|
2480
2523
|
const estimatedCost = estimateSearchCost(body);
|
|
2481
|
-
const
|
|
2482
|
-
if (!
|
|
2524
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2525
|
+
if (!gate.allowed) {
|
|
2483
2526
|
return {
|
|
2484
|
-
content: [{ type: "text", text: `${
|
|
2527
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2485
2528
|
isError: true
|
|
2486
2529
|
};
|
|
2487
2530
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2531
|
+
try {
|
|
2532
|
+
const client = getClient();
|
|
2533
|
+
const endpoint = cleanPath ? `/v1/search/${cleanPath}` : "/v1/search";
|
|
2534
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2535
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2536
|
+
return {
|
|
2537
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2538
|
+
structuredContent: asStructuredContent(result)
|
|
2539
|
+
};
|
|
2540
|
+
} finally {
|
|
2541
|
+
gate.release();
|
|
2542
|
+
}
|
|
2497
2543
|
} catch (err) {
|
|
2498
2544
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2499
2545
|
}
|
|
@@ -2535,23 +2581,30 @@ Full request/response shapes + worked research workflows in the \`exa-research\`
|
|
|
2535
2581
|
async ({ path: path5, body, agent_id }) => {
|
|
2536
2582
|
try {
|
|
2537
2583
|
body = coerceBody(body);
|
|
2584
|
+
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/exa\//, "");
|
|
2585
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2586
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2587
|
+
}
|
|
2538
2588
|
const estimatedCost = estimateExaCost(path5, body);
|
|
2539
|
-
const
|
|
2540
|
-
if (!
|
|
2589
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2590
|
+
if (!gate.allowed) {
|
|
2541
2591
|
return {
|
|
2542
|
-
content: [{ type: "text", text: `${
|
|
2592
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2543
2593
|
isError: true
|
|
2544
2594
|
};
|
|
2545
2595
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2596
|
+
try {
|
|
2597
|
+
const client = getClient();
|
|
2598
|
+
const endpoint = `/v1/exa/${cleanPath}`;
|
|
2599
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2600
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2601
|
+
return {
|
|
2602
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2603
|
+
structuredContent: asStructuredContent(result)
|
|
2604
|
+
};
|
|
2605
|
+
} finally {
|
|
2606
|
+
gate.release();
|
|
2607
|
+
}
|
|
2555
2608
|
} catch (err) {
|
|
2556
2609
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2557
2610
|
}
|
|
@@ -2634,21 +2687,28 @@ Pass query params via 'params' (GET). Use 'body' only for POST endpoints (e.g. p
|
|
|
2634
2687
|
async ({ path: path5, params, body, agent_id }) => {
|
|
2635
2688
|
try {
|
|
2636
2689
|
body = coerceBody(body);
|
|
2690
|
+
if (hasPathTraversal(path5)) {
|
|
2691
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2692
|
+
}
|
|
2637
2693
|
const estimatedCost = estimateMarketCost(path5, body);
|
|
2638
|
-
const
|
|
2639
|
-
if (!
|
|
2694
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2695
|
+
if (!gate.allowed) {
|
|
2640
2696
|
return {
|
|
2641
|
-
content: [{ type: "text", text: `${
|
|
2697
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2642
2698
|
isError: true
|
|
2643
2699
|
};
|
|
2644
2700
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2701
|
+
try {
|
|
2702
|
+
const llm = getClient();
|
|
2703
|
+
const result = body !== void 0 ? await llm.pmQuery(path5, body) : await llm.pm(path5, params);
|
|
2704
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2705
|
+
return {
|
|
2706
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2707
|
+
structuredContent: asStructuredContent(result)
|
|
2708
|
+
};
|
|
2709
|
+
} finally {
|
|
2710
|
+
gate.release();
|
|
2711
|
+
}
|
|
2652
2712
|
} catch (err) {
|
|
2653
2713
|
return {
|
|
2654
2714
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2730,51 +2790,55 @@ Examples:
|
|
|
2730
2790
|
};
|
|
2731
2791
|
}
|
|
2732
2792
|
const estimatedCost = paid ? 1e-3 : 0;
|
|
2733
|
-
const
|
|
2734
|
-
if (!
|
|
2793
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2794
|
+
if (!gate.allowed) {
|
|
2735
2795
|
return {
|
|
2736
|
-
content: [{ type: "text", text: `${
|
|
2796
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2737
2797
|
isError: true
|
|
2738
2798
|
};
|
|
2739
2799
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
if (
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
if (
|
|
2755
|
-
|
|
2756
|
-
|
|
2800
|
+
try {
|
|
2801
|
+
const priceClient = getPriceClient(paid);
|
|
2802
|
+
if (action === "price") {
|
|
2803
|
+
if (!symbol) throw new Error("symbol is required for action='price'");
|
|
2804
|
+
const result2 = await priceClient.price(category, symbol, {
|
|
2805
|
+
market,
|
|
2806
|
+
session
|
|
2807
|
+
});
|
|
2808
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2809
|
+
return {
|
|
2810
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2811
|
+
structuredContent: result2
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
if (action === "history") {
|
|
2815
|
+
if (!symbol) throw new Error("symbol is required for action='history'");
|
|
2816
|
+
if (from === void 0) throw new Error("from (unix seconds) is required for action='history'");
|
|
2817
|
+
const result2 = await priceClient.history(category, symbol, {
|
|
2818
|
+
market,
|
|
2819
|
+
session,
|
|
2820
|
+
resolution: resolution ?? "D",
|
|
2821
|
+
from,
|
|
2822
|
+
to
|
|
2823
|
+
});
|
|
2824
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2825
|
+
return {
|
|
2826
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }],
|
|
2827
|
+
structuredContent: result2
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
const result = await priceClient.listSymbols(category, {
|
|
2757
2831
|
market,
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
from,
|
|
2761
|
-
to
|
|
2832
|
+
query,
|
|
2833
|
+
limit
|
|
2762
2834
|
});
|
|
2763
|
-
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
2764
2835
|
return {
|
|
2765
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2766
|
-
structuredContent:
|
|
2836
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2837
|
+
structuredContent: result
|
|
2767
2838
|
};
|
|
2839
|
+
} finally {
|
|
2840
|
+
gate.release();
|
|
2768
2841
|
}
|
|
2769
|
-
const result = await priceClient.listSymbols(category, {
|
|
2770
|
-
market,
|
|
2771
|
-
query,
|
|
2772
|
-
limit
|
|
2773
|
-
});
|
|
2774
|
-
return {
|
|
2775
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2776
|
-
structuredContent: result
|
|
2777
|
-
};
|
|
2778
2842
|
} catch (err) {
|
|
2779
2843
|
return {
|
|
2780
2844
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -2905,22 +2969,29 @@ Full action shapes + GPU type details in the \`modal\` skill.`,
|
|
|
2905
2969
|
try {
|
|
2906
2970
|
body = coerceBody(body);
|
|
2907
2971
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/modal\//, "");
|
|
2972
|
+
if (hasPathTraversal(cleanPath)) {
|
|
2973
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
2974
|
+
}
|
|
2908
2975
|
const estimatedCost = estimateModalCost(cleanPath);
|
|
2909
|
-
const
|
|
2910
|
-
if (!
|
|
2976
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
2977
|
+
if (!gate.allowed) {
|
|
2911
2978
|
return {
|
|
2912
|
-
content: [{ type: "text", text: `${
|
|
2979
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2913
2980
|
isError: true
|
|
2914
2981
|
};
|
|
2915
2982
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2983
|
+
try {
|
|
2984
|
+
const client = buildClientWithTimeout(modalTimeoutMs(body));
|
|
2985
|
+
const endpoint = `/v1/modal/${cleanPath}`;
|
|
2986
|
+
const result = await client.requestWithPaymentRaw(endpoint, body ?? {});
|
|
2987
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
2988
|
+
return {
|
|
2989
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2990
|
+
structuredContent: asStructuredContent(result)
|
|
2991
|
+
};
|
|
2992
|
+
} finally {
|
|
2993
|
+
gate.release();
|
|
2994
|
+
}
|
|
2924
2995
|
} catch (err) {
|
|
2925
2996
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2926
2997
|
}
|
|
@@ -2971,22 +3042,29 @@ Voice call flow + voice preset details + full body shapes in the \`phone\` skill
|
|
|
2971
3042
|
try {
|
|
2972
3043
|
body = coerceBody(body);
|
|
2973
3044
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\//, "");
|
|
3045
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3046
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3047
|
+
}
|
|
2974
3048
|
const estimatedCost = estimatePhoneCost(cleanPath, body !== void 0);
|
|
2975
|
-
const
|
|
2976
|
-
if (!
|
|
3049
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3050
|
+
if (!gate.allowed) {
|
|
2977
3051
|
return {
|
|
2978
|
-
content: [{ type: "text", text: `${
|
|
3052
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
2979
3053
|
isError: true
|
|
2980
3054
|
};
|
|
2981
3055
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3056
|
+
try {
|
|
3057
|
+
const client = getClient();
|
|
3058
|
+
const endpoint = `/v1/${cleanPath}`;
|
|
3059
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint);
|
|
3060
|
+
if (estimatedCost > 0) recordSpending(budget, estimatedCost, agent_id);
|
|
3061
|
+
return {
|
|
3062
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3063
|
+
structuredContent: asStructuredContent(result)
|
|
3064
|
+
};
|
|
3065
|
+
} finally {
|
|
3066
|
+
gate.release();
|
|
3067
|
+
}
|
|
2990
3068
|
} catch (err) {
|
|
2991
3069
|
return { content: [{ type: "text", text: formatError(extractErrorMessage(err)) }], isError: true };
|
|
2992
3070
|
}
|
|
@@ -3069,22 +3147,29 @@ Each Surf endpoint pre-validates required params before settling \u2014 you get
|
|
|
3069
3147
|
try {
|
|
3070
3148
|
body = coerceBody(body);
|
|
3071
3149
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/surf\//, "").replace(/^api\/v1\/surf\//, "");
|
|
3150
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3151
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3152
|
+
}
|
|
3072
3153
|
const estimatedCost = estimateSurfCost(cleanPath);
|
|
3073
|
-
const
|
|
3074
|
-
if (!
|
|
3154
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3155
|
+
if (!gate.allowed) {
|
|
3075
3156
|
return {
|
|
3076
|
-
content: [{ type: "text", text: `${
|
|
3157
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3077
3158
|
isError: true
|
|
3078
3159
|
};
|
|
3079
3160
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3161
|
+
try {
|
|
3162
|
+
const client = getClient();
|
|
3163
|
+
const endpoint = `/v1/surf/${cleanPath}`;
|
|
3164
|
+
const result = body !== void 0 ? await client.requestWithPaymentRaw(endpoint, body) : await client.getWithPaymentRaw(endpoint, params);
|
|
3165
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3166
|
+
return {
|
|
3167
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3168
|
+
structuredContent: asStructuredContent(result)
|
|
3169
|
+
};
|
|
3170
|
+
} finally {
|
|
3171
|
+
gate.release();
|
|
3172
|
+
}
|
|
3088
3173
|
} catch (err) {
|
|
3089
3174
|
return {
|
|
3090
3175
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3138,21 +3223,31 @@ Prefer blockrun_price (free quotes), blockrun_dex (free DEX data), or blockrun_s
|
|
|
3138
3223
|
}
|
|
3139
3224
|
const batchCount = Array.isArray(body) ? Math.max(body.length, 1) : 1;
|
|
3140
3225
|
const estimatedCost = RPC_PRICE_USD * batchCount;
|
|
3141
|
-
const
|
|
3142
|
-
if (!
|
|
3226
|
+
const cleanNetwork = network.trim().toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
3227
|
+
if (!isValidNetworkSlug(cleanNetwork)) {
|
|
3143
3228
|
return {
|
|
3144
|
-
content: [{ type: "text", text:
|
|
3229
|
+
content: [{ type: "text", text: formatError(`Invalid network '${network}'. Use a chain slug like 'ethereum', 'base', or 'solana'.`) }],
|
|
3145
3230
|
isError: true
|
|
3146
3231
|
};
|
|
3147
3232
|
}
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3233
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3234
|
+
if (!gate.allowed) {
|
|
3235
|
+
return {
|
|
3236
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3237
|
+
isError: true
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
try {
|
|
3241
|
+
const client = getClient();
|
|
3242
|
+
const result = await client.requestWithPaymentRaw(`/v1/rpc/${cleanNetwork}`, body);
|
|
3243
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3244
|
+
return {
|
|
3245
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3246
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3247
|
+
};
|
|
3248
|
+
} finally {
|
|
3249
|
+
gate.release();
|
|
3250
|
+
}
|
|
3156
3251
|
} catch (err) {
|
|
3157
3252
|
return {
|
|
3158
3253
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3195,21 +3290,28 @@ Use blockrun_price (free) for plain spot quotes, blockrun_dex (free) for DEX pai
|
|
|
3195
3290
|
async ({ path: path5, agent_id }) => {
|
|
3196
3291
|
try {
|
|
3197
3292
|
const cleanPath = path5.replace(/^\/+/, "").replace(/^v1\/defillama\//, "").replace(/^api\/v1\/defillama\//, "");
|
|
3293
|
+
if (hasPathTraversal(cleanPath)) {
|
|
3294
|
+
return { content: [{ type: "text", text: formatError(`Invalid path '${path5}'.`) }], isError: true };
|
|
3295
|
+
}
|
|
3198
3296
|
const estimatedCost = estimateDefiCost(cleanPath);
|
|
3199
|
-
const
|
|
3200
|
-
if (!
|
|
3297
|
+
const gate = reserveBudget(budget, agent_id, estimatedCost);
|
|
3298
|
+
if (!gate.allowed) {
|
|
3201
3299
|
return {
|
|
3202
|
-
content: [{ type: "text", text: `${
|
|
3300
|
+
content: [{ type: "text", text: `${gate.reason}. Use blockrun_wallet action:"report" to see usage or action:"delegate" to increase agent budget.` }],
|
|
3203
3301
|
isError: true
|
|
3204
3302
|
};
|
|
3205
3303
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3304
|
+
try {
|
|
3305
|
+
const client = getClient();
|
|
3306
|
+
const result = await client.getWithPaymentRaw(`/v1/defillama/${cleanPath}`);
|
|
3307
|
+
recordSpending(budget, estimatedCost, agent_id);
|
|
3308
|
+
return {
|
|
3309
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3310
|
+
structuredContent: typeof result === "object" && result !== null && !Array.isArray(result) ? result : { result }
|
|
3311
|
+
};
|
|
3312
|
+
} finally {
|
|
3313
|
+
gate.release();
|
|
3314
|
+
}
|
|
3213
3315
|
} catch (err) {
|
|
3214
3316
|
return {
|
|
3215
3317
|
content: [{ type: "text", text: formatError(extractErrorMessage(err)) }],
|
|
@@ -3268,7 +3370,7 @@ function resolveProfileName(argv = process.argv.slice(2), env = process.env) {
|
|
|
3268
3370
|
}
|
|
3269
3371
|
function resolveTools(argv, env) {
|
|
3270
3372
|
const requested = resolveProfileName(argv, env);
|
|
3271
|
-
const spec = PROFILES[requested];
|
|
3373
|
+
const spec = Object.hasOwn(PROFILES, requested) ? PROFILES[requested] : void 0;
|
|
3272
3374
|
if (!spec) {
|
|
3273
3375
|
return { profile: DEFAULT_PROFILE, tools: new Set(ALL_TOOLS) };
|
|
3274
3376
|
}
|
|
@@ -3359,6 +3461,10 @@ function looksLikeRawPrivateKey(value) {
|
|
|
3359
3461
|
if (value.length >= 80 && value.length <= 100 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(value)) return true;
|
|
3360
3462
|
return false;
|
|
3361
3463
|
}
|
|
3464
|
+
function looksLikeNamedSecretValue(value) {
|
|
3465
|
+
if (looksLikeRawPrivateKey(value)) return true;
|
|
3466
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
3467
|
+
}
|
|
3362
3468
|
function looksLikeSolanaSecretKeyArray(value) {
|
|
3363
3469
|
return Array.isArray(value) && value.length === 64 && value.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 255);
|
|
3364
3470
|
}
|
|
@@ -3373,7 +3479,7 @@ function walk(obj, file, jsonPath, out) {
|
|
|
3373
3479
|
}
|
|
3374
3480
|
for (const [k, v] of Object.entries(obj)) {
|
|
3375
3481
|
const next = jsonPath ? `${jsonPath}.${k}` : k;
|
|
3376
|
-
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) &&
|
|
3482
|
+
if (/wallet[-_ ]?key|private[-_ ]?key|secret/i.test(k) && looksLikeNamedSecretValue(v)) {
|
|
3377
3483
|
out.push({ file, path: next });
|
|
3378
3484
|
} else if (looksLikeRawPrivateKey(v)) {
|
|
3379
3485
|
out.push({ file, path: next });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
|
|
5
5
|
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, crypto data, X/Twitter intelligence. Paid via x402 micropayments.",
|
|
6
6
|
"type": "module",
|