@blockrun/clawrouter 0.12.270 → 0.12.271

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/README.md CHANGED
@@ -444,11 +444,19 @@ Surf is BlockRun's unified crypto data API — **84 endpoints across 13 domains*
444
444
 
445
445
  ClawRouter ships Surf as a **skill, not as typed wrappers**. The proxy whitelists `/v1/surf/*` so any call through the local proxy is paid x402 from the same wallet; the agent reads `skills/surf/SKILL.md` for the endpoint catalog and crafts the HTTP call. No `blockrun_surf_*` tool definitions to maintain; a new Surf endpoint requires zero ClawRouter release.
446
446
 
447
- | Tier | Cost | Examples |
448
- | ---- | ---------: | ------------------------------------------------------------- |
449
- | 1 | **$0.001** | prices, rankings, lists, news |
450
- | 2 | **$0.005** | orderbooks, candles, search, wallet details, social mindshare |
451
- | 3 | **$0.020** | on-chain SQL / query / schema, chat completions |
447
+ **Flat per-call pricing** — every Surf endpoint costs the same. The old
448
+ $0.001/$0.005/$0.020 tiers no longer apply; upstream prices all three tiers
449
+ identically, and the tier now describes only endpoint weight.
450
+
451
+ | Rail | Price per call |
452
+ | --------------------------- | ---------------------------------- |
453
+ | API key (`api.blockrun.ai`) | **$0.0075** (base rate, no tx fee) |
454
+ | Wallet — Solana | **$0.0075** |
455
+ | Wallet — Base | **$0.0085** |
456
+
457
+ Measured 2026-09-05: the API-key rate from the account ledger, the wallet rates
458
+ from the x402 challenge, which quotes the price before anything is signed. On the
459
+ wallet rail that quote is always authoritative.
452
460
 
453
461
  **Usage (HTTP):**
454
462
 
@@ -729,7 +737,7 @@ For basic usage, no configuration needed. For advanced options:
729
737
  | `BLOCKRUN_PROXY_PORT` | `8402` | Local proxy port |
730
738
  | `CLAWROUTER_DISABLED` | `false` | Disable smart routing |
731
739
  | `CLAWROUTER_DEBUG_HEADERS` | `on` | Set to `off` to suppress `x-clawrouter-*` debug response headers |
732
- | `CLAWROUTER_SOLANA_RPC_URL` | `https://api.mainnet-beta.solana.com` | Solana RPC endpoint |
740
+ | `CLAWROUTER_SOLANA_RPC_URL` | `https://api.mainnet-beta.solana.com` | Solana RPC endpoint — balance checks and payment signing |
733
741
 
734
742
  **Full reference:** [docs/configuration.md](docs/configuration.md)
735
743
 
package/dist/cli.js CHANGED
@@ -40713,6 +40713,18 @@ function isBalanceError(error) {
40713
40713
  function isRpcError(error) {
40714
40714
  return error instanceof Error && error.code === "RPC_ERROR";
40715
40715
  }
40716
+ function describeFetchError(error) {
40717
+ const message = error instanceof Error ? error.message : String(error);
40718
+ const cause = error?.cause;
40719
+ if (!(cause instanceof Error)) return message;
40720
+ const detail = cause;
40721
+ const host = detail.hostname || detail.address;
40722
+ const where = host && detail.port ? `${host}:${detail.port}` : host;
40723
+ const parts = [detail.code, where, detail.code ? void 0 : detail.message].filter(
40724
+ (part) => typeof part === "string" && part !== ""
40725
+ );
40726
+ return parts.length > 0 ? `${message} (${parts.join(" ")})` : message;
40727
+ }
40716
40728
  var InsufficientFundsError2, EmptyWalletError, RpcError2;
40717
40729
  var init_errors5 = __esm({
40718
40730
  "src/errors.ts"() {
@@ -60152,6 +60164,40 @@ async function clearApiKey() {
60152
60164
  }
60153
60165
  return { removed, envStillSet: Boolean(process["env"].BLOCKRUN_API_KEY?.trim()) };
60154
60166
  }
60167
+ async function fetchCreditBalance(apiKey, timeoutMs = 5e3) {
60168
+ try {
60169
+ const controller = new AbortController();
60170
+ const timer2 = setTimeout(() => controller.abort(), timeoutMs);
60171
+ const res = await fetch(`${BLOCKRUN_API_KEY_API}/v1/credits`, {
60172
+ headers: { authorization: `Bearer ${apiKey}` },
60173
+ signal: controller.signal
60174
+ });
60175
+ clearTimeout(timer2);
60176
+ if (!res.ok) return void 0;
60177
+ const b = await res.json();
60178
+ const num2 = (v) => typeof v === "number" ? v : null;
60179
+ return {
60180
+ accountId: typeof b.account_id === "string" ? b.account_id : void 0,
60181
+ billingMode: typeof b.billing_mode === "string" ? b.billing_mode : void 0,
60182
+ currency: typeof b.currency === "string" ? b.currency : "USD",
60183
+ grantedUsd: num2(b.granted_usd),
60184
+ spentUsd: num2(b.spent_usd),
60185
+ remainingUsd: num2(b.remaining_usd),
60186
+ blocked: b.blocked === true,
60187
+ blockedReason: typeof b.blocked_reason === "string" ? b.blocked_reason : void 0
60188
+ };
60189
+ } catch {
60190
+ return void 0;
60191
+ }
60192
+ }
60193
+ function formatCreditBalance(b) {
60194
+ const money = (v) => v === null ? void 0 : `$${v.toFixed(v < 0.01 && v > 0 ? 6 : 2)}`;
60195
+ const remaining = money(b.remainingUsd);
60196
+ const spent = money(b.spentUsd);
60197
+ if (remaining) return `${remaining} remaining${spent ? ` (spent ${spent})` : ""}`;
60198
+ const mode = b.billingMode === "ungated" ? "no prepaid limit" : b.billingMode ?? "unknown";
60199
+ return `${spent ? `spent ${spent}` : "unknown"} \u2014 ${mode}`;
60200
+ }
60155
60201
  function createApiKeyFetch(apiKey, baseFetch = fetch) {
60156
60202
  return async (input, init2) => {
60157
60203
  const headers = new Headers(init2?.headers);
@@ -90673,6 +90719,24 @@ var init_client3 = __esm({
90673
90719
  }
90674
90720
  });
90675
90721
 
90722
+ // node_modules/@x402/svm/dist/esm/v1/index.mjs
90723
+ var v1_exports = {};
90724
+ __export(v1_exports, {
90725
+ ExactSvmSchemeV1: () => ExactSvmSchemeV1,
90726
+ NETWORKS: () => NETWORKS2
90727
+ });
90728
+ var init_v1 = __esm({
90729
+ "node_modules/@x402/svm/dist/esm/v1/index.mjs"() {
90730
+ "use strict";
90731
+ init_chunk_WWACQNRQ();
90732
+ init_chunk_MNNIECS4();
90733
+ init_chunk_WIPN332D();
90734
+ init_chunk_FVTMOTG6();
90735
+ init_chunk_FJ6REVHH();
90736
+ init_chunk_QEBTC3LJ();
90737
+ }
90738
+ });
90739
+
90676
90740
  // src/proxy.ts
90677
90741
  import { AsyncLocalStorage } from "async_hooks";
90678
90742
  import { createHmac } from "crypto";
@@ -91427,6 +91491,23 @@ function estimateImageCost(model, size5, n = 1) {
91427
91491
  const pricePerImage = sizePrice ?? pricing.default;
91428
91492
  return pricePerImage * n * 1.05;
91429
91493
  }
91494
+ function blockrunRequestId(response) {
91495
+ if (!response) return void 0;
91496
+ for (const name of ["x-blockrun-request-id", "x-request-id", "request-id"]) {
91497
+ const value = response.headers.get(name)?.trim();
91498
+ if (value) return value;
91499
+ }
91500
+ return void 0;
91501
+ }
91502
+ function gatewaySettledCostUsd(response) {
91503
+ const raw = response?.headers.get("x-blockrun-cost-usd");
91504
+ if (raw === null || raw === void 0) return void 0;
91505
+ const trimmed = raw.trim();
91506
+ if (trimmed === "") return void 0;
91507
+ const value = Number(trimmed);
91508
+ if (!Number.isFinite(value) || value < 0) return void 0;
91509
+ return value;
91510
+ }
91430
91511
  async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPaymentUsd) {
91431
91512
  const startTime = Date.now();
91432
91513
  const upstreamUrl = `${apiBase}${req.url}`;
@@ -91476,8 +91557,9 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91476
91557
  res.end();
91477
91558
  const latencyMs = Date.now() - startTime;
91478
91559
  console.log(`[ClawRouter] ${requestLabel} response: ${upstream.status} (${latencyMs}ms)`);
91479
- const paidAmount = getActualPaymentUsd();
91480
- const requestCost = resolvePhoneTelemetryCost({
91560
+ const settledCostUsd = gatewaySettledCostUsd(upstream);
91561
+ const paidAmount = settledCostUsd ?? getActualPaymentUsd();
91562
+ const requestCost = settledCostUsd !== void 0 ? settledCostUsd : resolvePhoneTelemetryCost({
91481
91563
  paidAmount,
91482
91564
  isPhone,
91483
91565
  upstreamStatus: upstream.status,
@@ -91486,6 +91568,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91486
91568
  });
91487
91569
  logUsage({
91488
91570
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
91571
+ ...blockrunRequestId(upstream) ? { requestId: blockrunRequestId(upstream) } : {},
91489
91572
  model: isBlockrunExa ? "blockrun-exa" : isModalSandbox ? "modal-sandbox" : isPhone ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] : isSurf ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] : "partner",
91490
91573
  tier: isPhone ? "PHONE" : isSurf ? "SURF" : "PARTNER",
91491
91574
  cost: requestCost,
@@ -91677,8 +91760,20 @@ async function startProxy(options) {
91677
91760
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
91678
91761
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes2(solanaPrivateKeyBytes);
91679
91762
  solanaAddress = solanaSigner.address;
91763
+ const solanaRpcUrl = process["env"].CLAWROUTER_SOLANA_RPC_URL;
91680
91764
  registerExactSvmScheme2(x402, { signer: solanaSigner });
91765
+ if (solanaRpcUrl) {
91766
+ const { ExactSvmScheme: ExactSvmScheme2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
91767
+ const { ExactSvmSchemeV1: ExactSvmSchemeV12, NETWORKS: SVM_V1_NETWORKS } = await Promise.resolve().then(() => (init_v1(), v1_exports));
91768
+ x402.register("solana:*", new ExactSvmScheme2(solanaSigner, { rpcUrl: solanaRpcUrl }));
91769
+ for (const network of SVM_V1_NETWORKS) {
91770
+ x402.registerV1(network, new ExactSvmSchemeV12(solanaSigner, { rpcUrl: solanaRpcUrl }));
91771
+ }
91772
+ }
91681
91773
  console.log(`[ClawRouter] Solana wallet: ${solanaAddress}`);
91774
+ if (solanaRpcUrl) {
91775
+ console.log(`[ClawRouter] Solana RPC (payment signing + balance): ${solanaRpcUrl}`);
91776
+ }
91682
91777
  }
91683
91778
  x402?.onAfterPaymentCreation(async (context) => {
91684
91779
  if (!context.selectedRequirements.network.startsWith("eip155")) return;
@@ -91778,7 +91873,15 @@ async function startProxy(options) {
91778
91873
  )
91779
91874
  ]);
91780
91875
  if (authMode === "api-key") {
91781
- response.balance = null;
91876
+ const credit = await fetchCreditBalance(apiKey, BALANCE_CHECK_TIMEOUT_MS);
91877
+ response.balance = credit?.remainingUsd ?? null;
91878
+ response.creditSpent = credit?.spentUsd ?? null;
91879
+ response.creditGranted = credit?.grantedUsd ?? null;
91880
+ response.billingMode = credit?.billingMode ?? null;
91881
+ response.creditSummary = credit ? formatCreditBalance(credit) : null;
91882
+ response.blocked = credit?.blocked ?? false;
91883
+ if (credit?.blockedReason) response.blockedReason = credit.blockedReason;
91884
+ response.isEmpty = credit?.remainingUsd !== null && (credit?.remainingUsd ?? 1) <= 0;
91782
91885
  response.billing = "BlockRun account credit";
91783
91886
  response.topUpUrl = PORTAL_CREDITS_URL;
91784
91887
  } else {
@@ -92883,7 +92986,8 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
92883
92986
  errorBody,
92884
92987
  errorStatus: response.status,
92885
92988
  isProviderError: category !== null,
92886
- errorCategory: category ?? void 0
92989
+ errorCategory: category ?? void 0,
92990
+ upstreamRequestId: blockrunRequestId(response)
92887
92991
  };
92888
92992
  }
92889
92993
  const contentType = response.headers.get("content-type") || "";
@@ -92920,9 +93024,12 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
92920
93024
  errorCategory: "payment_error"
92921
93025
  };
92922
93026
  }
93027
+ const networkMsg = describeFetchError(err);
93028
+ const solanaSigningHint = upstreamUrl.startsWith(BLOCKRUN_SOLANA_API) && networkMsg.startsWith("fetch failed") ? " \u2014 on the Solana rail this may be the RPC that signs the payment rather than the gateway; if api.mainnet-beta.solana.com is unreachable from this host, set CLAWROUTER_SOLANA_RPC_URL to your own endpoint" : "";
93029
+ console.error(`[ClawRouter] ${modelId} network error: ${networkMsg}${solanaSigningHint}`);
92923
93030
  return {
92924
93031
  success: false,
92925
- errorBody: errorMsg,
93032
+ errorBody: `${networkMsg}${solanaSigningHint}`,
92926
93033
  errorStatus: 500,
92927
93034
  isProviderError: true
92928
93035
  // Network errors are retryable
@@ -92955,6 +93062,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
92955
93062
  let accumulatedContent = "";
92956
93063
  let responseInputTokens;
92957
93064
  let responseOutputTokens;
93065
+ let upstreamRequestId;
92958
93066
  let requestHadError = false;
92959
93067
  let requestSummaryForStore = "";
92960
93068
  const isChatCompletion = req.url?.includes("/chat/completions");
@@ -94361,7 +94469,8 @@ data: [DONE]
94361
94469
  }
94362
94470
  lastError = {
94363
94471
  body: result.errorBody || "Unknown error",
94364
- status: result.errorStatus || 500
94472
+ status: result.errorStatus || 500,
94473
+ requestId: result.upstreamRequestId
94365
94474
  };
94366
94475
  failedAttempts.push({
94367
94476
  model: tryModel,
@@ -94433,7 +94542,8 @@ data: [DONE]
94433
94542
  }
94434
94543
  lastError = {
94435
94544
  body: retryResult.errorBody || lastError?.body || "Unknown error",
94436
- status: retryResult.errorStatus || lastError?.status || 500
94545
+ status: retryResult.errorStatus || lastError?.status || 500,
94546
+ requestId: retryResult.upstreamRequestId ?? lastError?.requestId
94437
94547
  };
94438
94548
  failedAttempts.push({
94439
94549
  model: tryModel,
@@ -94575,10 +94685,13 @@ data: [DONE]
94575
94685
  }
94576
94686
  }
94577
94687
  }
94688
+ upstreamRequestId = blockrunRequestId(upstream);
94578
94689
  if (!upstream) {
94579
94690
  const attemptSummary = failedAttempts.length > 0 ? failedAttempts.map((a) => `${a.model} (${a.reason})`).join(", ") : "unknown";
94580
94691
  const structuredMessage = failedAttempts.length > 0 ? `All ${failedAttempts.length} models failed. Tried: ${attemptSummary}` : "All models in fallback chain failed";
94581
- console.log(`[ClawRouter] ${structuredMessage}`);
94692
+ console.log(
94693
+ `[ClawRouter] ${structuredMessage}${lastError?.requestId ? ` | gateway request id: ${lastError.requestId}` : ""}`
94694
+ );
94582
94695
  const rawErrBody = lastError?.body || structuredMessage;
94583
94696
  const errStatus = lastError?.status || 502;
94584
94697
  const transformedErr = transformPaymentError(rawErrBody);
@@ -94616,7 +94729,8 @@ data: [DONE]
94616
94729
  res.writeHead(errStatus, {
94617
94730
  "Content-Type": "application/json",
94618
94731
  "x-context-used-kb": String(originalContextSizeKB),
94619
- "x-context-limit-kb": String(CONTEXT_LIMIT_KB)
94732
+ "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
94733
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
94620
94734
  });
94621
94735
  res.end(transformedErr);
94622
94736
  deduplicator.complete(dedupKey, {
@@ -95080,12 +95194,24 @@ data: [DONE]
95080
95194
  maxTokens,
95081
95195
  routingProfile ?? void 0
95082
95196
  );
95083
- logCost = FREE_MODELS.has(logModel) ? 0 : costs.costEstimate;
95197
+ const apiKeyPricing = options.apiKey ? routerOpts.modelPricing.get(logModel) : void 0;
95198
+ if (FREE_MODELS.has(logModel)) {
95199
+ logCost = 0;
95200
+ logSavings = 1;
95201
+ } else if (apiKeyPricing) {
95202
+ const inTokens = responseInputTokens ?? chargedInputTokens;
95203
+ const outTokens = responseOutputTokens ?? 0;
95204
+ logCost = (inTokens * (apiKeyPricing.inputPrice ?? 0) + outTokens * (apiKeyPricing.outputPrice ?? 0)) / 1e6;
95205
+ logSavings = costs.baselineCost > 0 ? Math.max(0, (costs.baselineCost - logCost) / costs.baselineCost) : 0;
95206
+ } else {
95207
+ logCost = costs.costEstimate;
95208
+ logSavings = costs.savings;
95209
+ }
95084
95210
  logBaseline = costs.baselineCost;
95085
- logSavings = FREE_MODELS.has(logModel) ? 1 : costs.savings;
95086
95211
  }
95087
95212
  const entry = {
95088
95213
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
95214
+ ...upstreamRequestId ? { requestId: upstreamRequestId } : {},
95089
95215
  model: logModel,
95090
95216
  tier: routingDecision?.tier ?? "DIRECT",
95091
95217
  cost: logCost,
@@ -95125,6 +95251,7 @@ var init_proxy = __esm({
95125
95251
  init_api_key();
95126
95252
  init_spend_control();
95127
95253
  init_compression();
95254
+ init_errors5();
95128
95255
  init_version4();
95129
95256
  init_session();
95130
95257
  init_updater();
@@ -230609,6 +230736,12 @@ ClawRouter Status (port ${port})
230609
230736
  console.log(` Auth: BlockRun API key ${data.apiKey}`);
230610
230737
  console.log(` Gateway: ${data.gateway}`);
230611
230738
  console.log(` Billing: account credit \u2014 top up at ${PORTAL_CREDITS_URL}`);
230739
+ if (data.creditSummary) console.log(` Credit: ${data.creditSummary}`);
230740
+ if (data.blocked) {
230741
+ console.log(` \u26A0 Account is blocked${data.blockedReason ? `: ${data.blockedReason}` : ""}`);
230742
+ } else if (data.isEmpty) {
230743
+ console.log(` \u26A0 Credit exhausted \u2014 top up at ${PORTAL_CREDITS_URL}`);
230744
+ }
230612
230745
  } else {
230613
230746
  console.log(` Auth: wallet (x402)`);
230614
230747
  console.log(` Payment Chain: ${data.paymentChain}`);
@@ -230636,6 +230769,7 @@ ClawRouter Account
230636
230769
  console.log(` Auth: BlockRun API key ${data.apiKey}`);
230637
230770
  console.log(` Gateway: ${data.gateway}`);
230638
230771
  console.log(` Billing: account credit (no wallet, no chain)`);
230772
+ if (data.creditSummary) console.log(` Credit: ${data.creditSummary}`);
230639
230773
  console.log(` Top up: ${PORTAL_CREDITS_URL}`);
230640
230774
  console.log(` Usage: ${PORTAL_URL}/dashboard`);
230641
230775
  console.log();
package/dist/index.d.ts CHANGED
@@ -1416,6 +1416,23 @@ type UsageEntry = {
1416
1416
  partnerId?: string;
1417
1417
  /** Partner service name (e.g., "BlockRun") — only set for partner API calls */
1418
1418
  service?: string;
1419
+ /**
1420
+ * The gateway's own id for this request, from the `x-blockrun-request-id`
1421
+ * response header. Absent for requests that never reached the gateway (a
1422
+ * cache hit, a local refusal, an aborted call).
1423
+ *
1424
+ * This is the JOIN KEY for billing reconciliation. Every `cost` in this file
1425
+ * is a LOCAL estimate computed from our copy of the price table, so it drifts
1426
+ * whenever that copy goes stale — six prices were wrong until v0.12.270, and
1427
+ * `/stats` had been reporting the wrong money the whole time. Recording the
1428
+ * gateway's id is what will let us diff this journal line-by-line against a
1429
+ * server-side ledger and show what was actually charged instead of what we
1430
+ * guessed. It cannot be backfilled: a call whose id we did not record is
1431
+ * unreconcilable forever, which is why this is captured before the ledger API
1432
+ * that will consume it exists. It is also the id support needs to trace one
1433
+ * failed call.
1434
+ */
1435
+ requestId?: string;
1419
1436
  };
1420
1437
  /**
1421
1438
  * Log a usage entry as a JSON line.
package/dist/index.js CHANGED
@@ -40581,6 +40581,18 @@ function isBalanceError(error) {
40581
40581
  function isRpcError(error) {
40582
40582
  return error instanceof Error && error.code === "RPC_ERROR";
40583
40583
  }
40584
+ function describeFetchError(error) {
40585
+ const message = error instanceof Error ? error.message : String(error);
40586
+ const cause = error?.cause;
40587
+ if (!(cause instanceof Error)) return message;
40588
+ const detail = cause;
40589
+ const host = detail.hostname || detail.address;
40590
+ const where = host && detail.port ? `${host}:${detail.port}` : host;
40591
+ const parts = [detail.code, where, detail.code ? void 0 : detail.message].filter(
40592
+ (part) => typeof part === "string" && part !== ""
40593
+ );
40594
+ return parts.length > 0 ? `${message} (${parts.join(" ")})` : message;
40595
+ }
40584
40596
  var InsufficientFundsError2, EmptyWalletError, RpcError2;
40585
40597
  var init_errors5 = __esm({
40586
40598
  "src/errors.ts"() {
@@ -59939,6 +59951,40 @@ async function resolveApiKey() {
59939
59951
  }
59940
59952
  return void 0;
59941
59953
  }
59954
+ async function fetchCreditBalance(apiKey, timeoutMs = 5e3) {
59955
+ try {
59956
+ const controller = new AbortController();
59957
+ const timer2 = setTimeout(() => controller.abort(), timeoutMs);
59958
+ const res = await fetch(`${BLOCKRUN_API_KEY_API}/v1/credits`, {
59959
+ headers: { authorization: `Bearer ${apiKey}` },
59960
+ signal: controller.signal
59961
+ });
59962
+ clearTimeout(timer2);
59963
+ if (!res.ok) return void 0;
59964
+ const b = await res.json();
59965
+ const num2 = (v) => typeof v === "number" ? v : null;
59966
+ return {
59967
+ accountId: typeof b.account_id === "string" ? b.account_id : void 0,
59968
+ billingMode: typeof b.billing_mode === "string" ? b.billing_mode : void 0,
59969
+ currency: typeof b.currency === "string" ? b.currency : "USD",
59970
+ grantedUsd: num2(b.granted_usd),
59971
+ spentUsd: num2(b.spent_usd),
59972
+ remainingUsd: num2(b.remaining_usd),
59973
+ blocked: b.blocked === true,
59974
+ blockedReason: typeof b.blocked_reason === "string" ? b.blocked_reason : void 0
59975
+ };
59976
+ } catch {
59977
+ return void 0;
59978
+ }
59979
+ }
59980
+ function formatCreditBalance(b) {
59981
+ const money = (v) => v === null ? void 0 : `$${v.toFixed(v < 0.01 && v > 0 ? 6 : 2)}`;
59982
+ const remaining = money(b.remainingUsd);
59983
+ const spent = money(b.spentUsd);
59984
+ if (remaining) return `${remaining} remaining${spent ? ` (spent ${spent})` : ""}`;
59985
+ const mode = b.billingMode === "ungated" ? "no prepaid limit" : b.billingMode ?? "unknown";
59986
+ return `${spent ? `spent ${spent}` : "unknown"} \u2014 ${mode}`;
59987
+ }
59942
59988
  function createApiKeyFetch(apiKey, baseFetch = fetch) {
59943
59989
  return async (input, init2) => {
59944
59990
  const headers = new Headers(init2?.headers);
@@ -90460,6 +90506,24 @@ var init_client3 = __esm({
90460
90506
  }
90461
90507
  });
90462
90508
 
90509
+ // node_modules/@x402/svm/dist/esm/v1/index.mjs
90510
+ var v1_exports = {};
90511
+ __export(v1_exports, {
90512
+ ExactSvmSchemeV1: () => ExactSvmSchemeV1,
90513
+ NETWORKS: () => NETWORKS2
90514
+ });
90515
+ var init_v1 = __esm({
90516
+ "node_modules/@x402/svm/dist/esm/v1/index.mjs"() {
90517
+ "use strict";
90518
+ init_chunk_WWACQNRQ();
90519
+ init_chunk_MNNIECS4();
90520
+ init_chunk_WIPN332D();
90521
+ init_chunk_FVTMOTG6();
90522
+ init_chunk_FJ6REVHH();
90523
+ init_chunk_QEBTC3LJ();
90524
+ }
90525
+ });
90526
+
90463
90527
  // src/proxy.ts
90464
90528
  import { AsyncLocalStorage } from "async_hooks";
90465
90529
  import { createHmac } from "crypto";
@@ -91214,6 +91278,23 @@ function estimateImageCost(model, size5, n = 1) {
91214
91278
  const pricePerImage = sizePrice ?? pricing.default;
91215
91279
  return pricePerImage * n * 1.05;
91216
91280
  }
91281
+ function blockrunRequestId(response) {
91282
+ if (!response) return void 0;
91283
+ for (const name of ["x-blockrun-request-id", "x-request-id", "request-id"]) {
91284
+ const value = response.headers.get(name)?.trim();
91285
+ if (value) return value;
91286
+ }
91287
+ return void 0;
91288
+ }
91289
+ function gatewaySettledCostUsd(response) {
91290
+ const raw = response?.headers.get("x-blockrun-cost-usd");
91291
+ if (raw === null || raw === void 0) return void 0;
91292
+ const trimmed = raw.trim();
91293
+ if (trimmed === "") return void 0;
91294
+ const value = Number(trimmed);
91295
+ if (!Number.isFinite(value) || value < 0) return void 0;
91296
+ return value;
91297
+ }
91217
91298
  async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPaymentUsd) {
91218
91299
  const startTime = Date.now();
91219
91300
  const upstreamUrl = `${apiBase}${req.url}`;
@@ -91263,8 +91344,9 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91263
91344
  res.end();
91264
91345
  const latencyMs = Date.now() - startTime;
91265
91346
  console.log(`[ClawRouter] ${requestLabel} response: ${upstream.status} (${latencyMs}ms)`);
91266
- const paidAmount = getActualPaymentUsd();
91267
- const requestCost = resolvePhoneTelemetryCost({
91347
+ const settledCostUsd = gatewaySettledCostUsd(upstream);
91348
+ const paidAmount = settledCostUsd ?? getActualPaymentUsd();
91349
+ const requestCost = settledCostUsd !== void 0 ? settledCostUsd : resolvePhoneTelemetryCost({
91268
91350
  paidAmount,
91269
91351
  isPhone,
91270
91352
  upstreamStatus: upstream.status,
@@ -91273,6 +91355,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91273
91355
  });
91274
91356
  logUsage({
91275
91357
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
91358
+ ...blockrunRequestId(upstream) ? { requestId: blockrunRequestId(upstream) } : {},
91276
91359
  model: isBlockrunExa ? "blockrun-exa" : isModalSandbox ? "modal-sandbox" : isPhone ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] : isSurf ? (req.url ?? "").replace(/^\/v1\//, "").split("?")[0] : "partner",
91277
91360
  tier: isPhone ? "PHONE" : isSurf ? "SURF" : "PARTNER",
91278
91361
  cost: requestCost,
@@ -91464,8 +91547,20 @@ async function startProxy(options) {
91464
91547
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
91465
91548
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes2(solanaPrivateKeyBytes);
91466
91549
  solanaAddress = solanaSigner.address;
91550
+ const solanaRpcUrl = process["env"].CLAWROUTER_SOLANA_RPC_URL;
91467
91551
  registerExactSvmScheme2(x402, { signer: solanaSigner });
91552
+ if (solanaRpcUrl) {
91553
+ const { ExactSvmScheme: ExactSvmScheme2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
91554
+ const { ExactSvmSchemeV1: ExactSvmSchemeV12, NETWORKS: SVM_V1_NETWORKS } = await Promise.resolve().then(() => (init_v1(), v1_exports));
91555
+ x402.register("solana:*", new ExactSvmScheme2(solanaSigner, { rpcUrl: solanaRpcUrl }));
91556
+ for (const network of SVM_V1_NETWORKS) {
91557
+ x402.registerV1(network, new ExactSvmSchemeV12(solanaSigner, { rpcUrl: solanaRpcUrl }));
91558
+ }
91559
+ }
91468
91560
  console.log(`[ClawRouter] Solana wallet: ${solanaAddress}`);
91561
+ if (solanaRpcUrl) {
91562
+ console.log(`[ClawRouter] Solana RPC (payment signing + balance): ${solanaRpcUrl}`);
91563
+ }
91469
91564
  }
91470
91565
  x402?.onAfterPaymentCreation(async (context) => {
91471
91566
  if (!context.selectedRequirements.network.startsWith("eip155")) return;
@@ -91565,7 +91660,15 @@ async function startProxy(options) {
91565
91660
  )
91566
91661
  ]);
91567
91662
  if (authMode === "api-key") {
91568
- response.balance = null;
91663
+ const credit = await fetchCreditBalance(apiKey, BALANCE_CHECK_TIMEOUT_MS);
91664
+ response.balance = credit?.remainingUsd ?? null;
91665
+ response.creditSpent = credit?.spentUsd ?? null;
91666
+ response.creditGranted = credit?.grantedUsd ?? null;
91667
+ response.billingMode = credit?.billingMode ?? null;
91668
+ response.creditSummary = credit ? formatCreditBalance(credit) : null;
91669
+ response.blocked = credit?.blocked ?? false;
91670
+ if (credit?.blockedReason) response.blockedReason = credit.blockedReason;
91671
+ response.isEmpty = credit?.remainingUsd !== null && (credit?.remainingUsd ?? 1) <= 0;
91569
91672
  response.billing = "BlockRun account credit";
91570
91673
  response.topUpUrl = PORTAL_CREDITS_URL;
91571
91674
  } else {
@@ -92670,7 +92773,8 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
92670
92773
  errorBody,
92671
92774
  errorStatus: response.status,
92672
92775
  isProviderError: category !== null,
92673
- errorCategory: category ?? void 0
92776
+ errorCategory: category ?? void 0,
92777
+ upstreamRequestId: blockrunRequestId(response)
92674
92778
  };
92675
92779
  }
92676
92780
  const contentType = response.headers.get("content-type") || "";
@@ -92707,9 +92811,12 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
92707
92811
  errorCategory: "payment_error"
92708
92812
  };
92709
92813
  }
92814
+ const networkMsg = describeFetchError(err);
92815
+ const solanaSigningHint = upstreamUrl.startsWith(BLOCKRUN_SOLANA_API) && networkMsg.startsWith("fetch failed") ? " \u2014 on the Solana rail this may be the RPC that signs the payment rather than the gateway; if api.mainnet-beta.solana.com is unreachable from this host, set CLAWROUTER_SOLANA_RPC_URL to your own endpoint" : "";
92816
+ console.error(`[ClawRouter] ${modelId} network error: ${networkMsg}${solanaSigningHint}`);
92710
92817
  return {
92711
92818
  success: false,
92712
- errorBody: errorMsg,
92819
+ errorBody: `${networkMsg}${solanaSigningHint}`,
92713
92820
  errorStatus: 500,
92714
92821
  isProviderError: true
92715
92822
  // Network errors are retryable
@@ -92742,6 +92849,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
92742
92849
  let accumulatedContent = "";
92743
92850
  let responseInputTokens;
92744
92851
  let responseOutputTokens;
92852
+ let upstreamRequestId;
92745
92853
  let requestHadError = false;
92746
92854
  let requestSummaryForStore = "";
92747
92855
  const isChatCompletion = req.url?.includes("/chat/completions");
@@ -94148,7 +94256,8 @@ data: [DONE]
94148
94256
  }
94149
94257
  lastError = {
94150
94258
  body: result.errorBody || "Unknown error",
94151
- status: result.errorStatus || 500
94259
+ status: result.errorStatus || 500,
94260
+ requestId: result.upstreamRequestId
94152
94261
  };
94153
94262
  failedAttempts.push({
94154
94263
  model: tryModel,
@@ -94220,7 +94329,8 @@ data: [DONE]
94220
94329
  }
94221
94330
  lastError = {
94222
94331
  body: retryResult.errorBody || lastError?.body || "Unknown error",
94223
- status: retryResult.errorStatus || lastError?.status || 500
94332
+ status: retryResult.errorStatus || lastError?.status || 500,
94333
+ requestId: retryResult.upstreamRequestId ?? lastError?.requestId
94224
94334
  };
94225
94335
  failedAttempts.push({
94226
94336
  model: tryModel,
@@ -94362,10 +94472,13 @@ data: [DONE]
94362
94472
  }
94363
94473
  }
94364
94474
  }
94475
+ upstreamRequestId = blockrunRequestId(upstream);
94365
94476
  if (!upstream) {
94366
94477
  const attemptSummary = failedAttempts.length > 0 ? failedAttempts.map((a) => `${a.model} (${a.reason})`).join(", ") : "unknown";
94367
94478
  const structuredMessage = failedAttempts.length > 0 ? `All ${failedAttempts.length} models failed. Tried: ${attemptSummary}` : "All models in fallback chain failed";
94368
- console.log(`[ClawRouter] ${structuredMessage}`);
94479
+ console.log(
94480
+ `[ClawRouter] ${structuredMessage}${lastError?.requestId ? ` | gateway request id: ${lastError.requestId}` : ""}`
94481
+ );
94369
94482
  const rawErrBody = lastError?.body || structuredMessage;
94370
94483
  const errStatus = lastError?.status || 502;
94371
94484
  const transformedErr = transformPaymentError(rawErrBody);
@@ -94403,7 +94516,8 @@ data: [DONE]
94403
94516
  res.writeHead(errStatus, {
94404
94517
  "Content-Type": "application/json",
94405
94518
  "x-context-used-kb": String(originalContextSizeKB),
94406
- "x-context-limit-kb": String(CONTEXT_LIMIT_KB)
94519
+ "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
94520
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
94407
94521
  });
94408
94522
  res.end(transformedErr);
94409
94523
  deduplicator.complete(dedupKey, {
@@ -94867,12 +94981,24 @@ data: [DONE]
94867
94981
  maxTokens,
94868
94982
  routingProfile ?? void 0
94869
94983
  );
94870
- logCost = FREE_MODELS.has(logModel) ? 0 : costs.costEstimate;
94984
+ const apiKeyPricing = options.apiKey ? routerOpts.modelPricing.get(logModel) : void 0;
94985
+ if (FREE_MODELS.has(logModel)) {
94986
+ logCost = 0;
94987
+ logSavings = 1;
94988
+ } else if (apiKeyPricing) {
94989
+ const inTokens = responseInputTokens ?? chargedInputTokens;
94990
+ const outTokens = responseOutputTokens ?? 0;
94991
+ logCost = (inTokens * (apiKeyPricing.inputPrice ?? 0) + outTokens * (apiKeyPricing.outputPrice ?? 0)) / 1e6;
94992
+ logSavings = costs.baselineCost > 0 ? Math.max(0, (costs.baselineCost - logCost) / costs.baselineCost) : 0;
94993
+ } else {
94994
+ logCost = costs.costEstimate;
94995
+ logSavings = costs.savings;
94996
+ }
94871
94997
  logBaseline = costs.baselineCost;
94872
- logSavings = FREE_MODELS.has(logModel) ? 1 : costs.savings;
94873
94998
  }
94874
94999
  const entry = {
94875
95000
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
95001
+ ...upstreamRequestId ? { requestId: upstreamRequestId } : {},
94876
95002
  model: logModel,
94877
95003
  tier: routingDecision?.tier ?? "DIRECT",
94878
95004
  cost: logCost,
@@ -94912,6 +95038,7 @@ var init_proxy = __esm({
94912
95038
  init_api_key();
94913
95039
  init_spend_control();
94914
95040
  init_compression();
95041
+ init_errors5();
94915
95042
  init_version4();
94916
95043
  init_session();
94917
95044
  init_updater();
@@ -209,7 +209,8 @@ Per-request alternative: send `x-clawrouter-debug: false` on the request.
209
209
 
210
210
  ### CLAWROUTER_SOLANA_RPC_URL
211
211
 
212
- Override the Solana RPC endpoint used for USDC balance checks (Solana chain only):
212
+ Override the Solana RPC endpoint used for USDC balance checks **and for signing
213
+ payments** (Solana chain only):
213
214
 
214
215
  ```bash
215
216
  export CLAWROUTER_SOLANA_RPC_URL=https://your-rpc-provider.com
@@ -218,6 +219,21 @@ openclaw gateway restart
218
219
 
219
220
  Public RPC may rate-limit on heavy usage. Use a dedicated RPC for production.
220
221
 
222
+ Unlike Base, where the EIP-3009 signature is produced entirely offline, signing
223
+ a Solana payment reads the payment asset's mint account over RPC. A host that
224
+ cannot reach `api.mainnet-beta.solana.com` therefore fails **every** paid Solana
225
+ call with `fetch failed` while `sol.blockrun.ai` itself answers normally. Check
226
+ egress before blaming the gateway:
227
+
228
+ ```bash
229
+ curl -s -m 10 -X POST https://api.mainnet-beta.solana.com \
230
+ -H 'content-type: application/json' \
231
+ -d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo","params":["EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",{"encoding":"base64"}]}'
232
+ ```
233
+
234
+ If that hangs or is refused, point this variable at an endpoint the host can
235
+ reach.
236
+
221
237
  ---
222
238
 
223
239
  ## Wallet Configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.270",
3
+ "version": "0.12.271",
4
4
  "description": "Smart LLM router — save 84% on inference costs. 76 models (7 free). Pay by credit card (API key) or USDC (x402 wallet).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,11 +26,27 @@ license: MIT
26
26
 
27
27
  Surf bundles **83 endpoints across 12 domains** into one paid HTTP API. ClawRouter exposes them at `http://127.0.0.1:8402/v1/surf/*`, paid the same way ClawRouter pays for LLM calls — an x402 USDC micropayment from the local wallet, or a draw on BlockRun account credit if an API key is configured (verified working on both, 2026-09-05). **No Surf account and no Surf API key either way.** Upstream lives at `api.asksurf.ai/gateway/v1` — ClawRouter forwards transparently.
28
28
 
29
- **Pricing tiers (per call):**
30
-
31
- - **Tier 1 — $0.001** — prices, rankings, lists, news, simple reads
32
- - **Tier 2 — $0.005** — orderbooks, candles, search, wallet details, social
33
- - **Tier 3 — $0.020** — on-chain SQL queries, structured queries, schema introspection
29
+ **Pricing — flat per call, the same for every endpoint.**
30
+
31
+ Surf used to be tiered at $0.001 / $0.005 / $0.020. It is not any more: upstream
32
+ now prices all three tiers identically (`SURF_TIER_{1,2,3}_PRICE` are equal). The
33
+ `Tier` column in the catalog below still describes endpoint weight, but it no
34
+ longer affects what you pay. Anything quoting the old tier prices is stale.
35
+
36
+ | Rail | Price per call |
37
+ | --------------------------- | ------------------------------------------- |
38
+ | API key (`api.blockrun.ai`) | **$0.0075** — base rate, no transaction fee |
39
+ | Wallet, Solana | **$0.0075** |
40
+ | Wallet, Base | **$0.0085** |
41
+
42
+ Verified 2026-09-05 by measurement, not from a price page: the API-key figure by
43
+ diffing the account ledger across one call, the wallet figures by reading the
44
+ `amount` in the x402 402 challenge, which quotes the price before anything is
45
+ signed. Base and Solana genuinely differ for the same endpoint.
46
+
47
+ **The 402 quote is always authoritative.** On the wallet rail the price arrives
48
+ in the challenge before you pay, so if this table ever disagrees with it, believe
49
+ the challenge and treat the table as stale.
34
50
 
35
51
  > The legacy **surf-1.5 chat** surface is intentionally NOT exposed yet — it's held until per-token settlement is wired. Trying `/v1/surf/chat/completions` returns 404 ("Unknown Surf endpoint"), no payment is taken.
36
52