@blockrun/clawrouter 0.12.273 → 0.12.275

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/cli.js CHANGED
@@ -32378,6 +32378,40 @@ function registerSpendPolicyHook(x402, control) {
32378
32378
  }
32379
32379
  });
32380
32380
  }
32381
+ function settledChargeUsd(response) {
32382
+ const raw = response.headers.get("x-blockrun-cost-usd");
32383
+ if (raw === null) return void 0;
32384
+ const trimmed = raw.trim();
32385
+ if (trimmed === "") return void 0;
32386
+ const value = Number(trimmed);
32387
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
32388
+ }
32389
+ function withSpendPolicy(inner, control, consumeEstimateUsd) {
32390
+ return async (input, init2) => {
32391
+ const published = consumeEstimateUsd();
32392
+ if (published === void 0 || !control.hasAmountLimits()) return inner(input, init2);
32393
+ const estimate = Number.isFinite(published) && published > 0 ? published : 0;
32394
+ const result = control.checkAmount(estimate);
32395
+ if (!result.allowed) {
32396
+ throw new SpendPolicyError(result.reason ?? "blocked by spend policy", {
32397
+ blockedBy: result.blockedBy
32398
+ });
32399
+ }
32400
+ const reservationId = control.hasAggregateLimits() ? control.reserve(estimate) : void 0;
32401
+ let response;
32402
+ try {
32403
+ response = await inner(input, init2);
32404
+ } catch (err) {
32405
+ if (reservationId !== void 0) control.releaseReservation(reservationId);
32406
+ throw err;
32407
+ }
32408
+ const settled = settledChargeUsd(response);
32409
+ const charged = response.ok ? settled ?? estimate : settled ?? 0;
32410
+ if (reservationId !== void 0) control.releaseReservation(reservationId);
32411
+ if (charged > 0) control.record(charged, { action: "BlockRun account charge" });
32412
+ return response;
32413
+ };
32414
+ }
32381
32415
  function formatDuration(seconds) {
32382
32416
  if (seconds < 60) {
32383
32417
  return `${seconds}s`;
@@ -32707,6 +32741,29 @@ var init_spend_control = __esm({
32707
32741
  };
32708
32742
  }
32709
32743
  }
32744
+ return this.checkAmount(estimatedCost2);
32745
+ }
32746
+ /**
32747
+ * The amount windows alone, with no counterparty to inspect.
32748
+ *
32749
+ * `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
32750
+ * say nothing about how the money moves, so they are the part of the policy
32751
+ * that means the same thing on the API-key rail as on the wallet rail — a
32752
+ * daily cap is as sensible against account credit as against USDC, and both
32753
+ * are read from the same `spending.json` (#329). The counterparty lists are
32754
+ * the part that does NOT translate: `blockedPayees`, `allowedPayees`,
32755
+ * `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
32756
+ * asset, and on the key rail there is one counterparty and no on-chain asset.
32757
+ * They are vacuous there rather than missing, which is why `check()` layers
32758
+ * them on top of this instead of the other way round.
32759
+ */
32760
+ checkAmount(estimatedCost2) {
32761
+ if (this.policyFileBroken !== void 0) {
32762
+ return {
32763
+ allowed: false,
32764
+ reason: `Spend policy is unreadable, refusing all payments: ${this.policyFileBroken}`
32765
+ };
32766
+ }
32710
32767
  const now2 = this.now();
32711
32768
  if (this.limits.perRequest !== void 0) {
32712
32769
  if (estimatedCost2 > this.limits.perRequest) {
@@ -59882,6 +59939,28 @@ Then run: npx @blockrun/clawrouter`
59882
59939
  solanaPrivateKeyBytes
59883
59940
  };
59884
59941
  }
59942
+ async function warnIfLegacyWalletDiffers(coreAddress) {
59943
+ if (legacyWalletDivergenceWarned) return;
59944
+ let legacyAddress;
59945
+ try {
59946
+ const saved = await loadSavedWallet();
59947
+ if (!saved) return;
59948
+ legacyAddress = privateKeyToAccount(saved).address;
59949
+ } catch {
59950
+ return;
59951
+ }
59952
+ if (legacyAddress === coreAddress) return;
59953
+ legacyWalletDivergenceWarned = true;
59954
+ console.warn(
59955
+ `[ClawRouter] \u26A0 Two different wallets are configured. Paying from the BlockRun Core wallet ${coreAddress}.`
59956
+ );
59957
+ console.warn(
59958
+ `[ClawRouter] ClawRouter's older wallet ${legacyAddress} (${WALLET_FILE}) is NOT being used \u2014 if that is the funded one, requests will fail on an empty balance.`
59959
+ );
59960
+ console.warn(
59961
+ `[ClawRouter] To keep paying from it: export BLOCKRUN_WALLET_KEY=$(cat ${WALLET_FILE})`
59962
+ );
59963
+ }
59885
59964
  async function resolveExistingWalletKey() {
59886
59965
  const coreSolanaKey = await loadCoreSolanaKeyForSelectedChain();
59887
59966
  const envKey = process["env"].BLOCKRUN_WALLET_KEY;
@@ -59897,6 +59976,7 @@ async function resolveExistingWalletKey() {
59897
59976
  const core = await loadCoreWallet();
59898
59977
  if (core) {
59899
59978
  const account = privateKeyToAccount(core);
59979
+ await warnIfLegacyWalletDiffers(account.address);
59900
59980
  return {
59901
59981
  key: core,
59902
59982
  address: account.address,
@@ -60064,13 +60144,28 @@ async function savePaymentChain(chain3) {
60064
60144
  }
60065
60145
  async function loadPaymentChain() {
60066
60146
  const core = await readOptional(CORE_CHAIN_FILE);
60067
- if (core === "solana") return "solana";
60068
- if (core === "base") return "base";
60069
60147
  const legacy = await readOptional(CHAIN_FILE);
60148
+ if (core === "solana" || core === "base") {
60149
+ if ((legacy === "solana" || legacy === "base") && legacy !== core) {
60150
+ warnChainOverride(core, legacy);
60151
+ }
60152
+ return core;
60153
+ }
60070
60154
  if (legacy === "solana") return "solana";
60071
60155
  if (legacy === "base") return "base";
60072
60156
  return await hasExistingWallet() ? "base" : "solana";
60073
60157
  }
60158
+ function warnChainOverride(core, legacy) {
60159
+ if (chainOverrideWarned) return;
60160
+ chainOverrideWarned = true;
60161
+ console.warn(
60162
+ `[ClawRouter] \u26A0 Payment chain is ${core} (from ${CORE_CHAIN_FILE}), overriding this install's saved ${legacy}.`
60163
+ );
60164
+ console.warn(
60165
+ `[ClawRouter] A different chain means a different signer and gateway \u2014 funds on ${legacy} will not be spendable.`
60166
+ );
60167
+ console.warn(`[ClawRouter] To go back: npx @blockrun/clawrouter wallet ${legacy}`);
60168
+ }
60074
60169
  async function hasExistingWallet() {
60075
60170
  if (process["env"].BLOCKRUN_WALLET_KEY?.trim()) return true;
60076
60171
  const [core, legacy] = await Promise.all([
@@ -60084,7 +60179,7 @@ async function resolvePaymentChain() {
60084
60179
  if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
60085
60180
  return loadPaymentChain();
60086
60181
  }
60087
- var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE;
60182
+ var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE, legacyWalletDivergenceWarned, chainOverrideWarned;
60088
60183
  var init_auth = __esm({
60089
60184
  "src/auth.ts"() {
60090
60185
  "use strict";
@@ -60100,6 +60195,8 @@ var init_auth = __esm({
60100
60195
  CORE_WALLET_FILE = join7(CORE_WALLET_DIR, ".session");
60101
60196
  CORE_SOLANA_WALLET_FILE = join7(CORE_WALLET_DIR, ".solana-session");
60102
60197
  CORE_CHAIN_FILE = join7(CORE_WALLET_DIR, ".chain");
60198
+ legacyWalletDivergenceWarned = false;
60199
+ chainOverrideWarned = false;
60103
60200
  }
60104
60201
  });
60105
60202
 
@@ -91608,6 +91705,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
91608
91705
  if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
91609
91706
  return estimate;
91610
91707
  }
91708
+ function publishDispatchCost(usd2) {
91709
+ const store = paymentStore.getStore();
91710
+ if (store) store.estimatedUsd = Number.isFinite(usd2) && usd2 > 0 ? usd2 : 0;
91711
+ }
91611
91712
  function noteRemainingCredit(response) {
91612
91713
  const remaining = gatewayRemainingCreditUsd(response);
91613
91714
  if (remaining === void 0) return;
@@ -91657,6 +91758,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91657
91758
  res.on("close", () => {
91658
91759
  if (!res.writableEnded) clientAbort.abort();
91659
91760
  });
91761
+ publishDispatchCost(0);
91660
91762
  const upstream = await payFetch(upstreamUrl, {
91661
91763
  method: req.method ?? "POST",
91662
91764
  headers,
@@ -91766,18 +91868,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
91766
91868
  let configured;
91767
91869
  try {
91768
91870
  const limits = (spendControl ?? new SpendControl()).getStatus().limits;
91769
- configured = ["perRequest", "hourly", "daily", "session"].filter((window2) => typeof limits[window2] === "number").map((window2) => `${window2}=$${limits[window2].toFixed(2)}`);
91871
+ configured = POLICY_LISTS.filter((list) => (limits[list]?.length ?? 0) > 0);
91770
91872
  } catch {
91771
91873
  return;
91772
91874
  }
91773
91875
  if (configured.length === 0) return;
91774
91876
  console.warn(
91775
- `[ClawRouter] \u26A0 Spend limits (${configured.join(", ")}) are NOT enforced in API-key mode \u2014 they gate x402 signing, and nothing is signed here.`
91877
+ `[ClawRouter] \u26A0 Counterparty policy (${configured.join(", ")}) does not apply in API-key mode \u2014 there is one counterparty and no on-chain asset.`
91776
91878
  );
91777
91879
  console.warn(
91778
- `[ClawRouter] Your BlockRun account balance is the cap instead. Use maxCostPerRun for a per-session limit that still applies.`
91880
+ `[ClawRouter] Your perRequest/hourly/daily/session limits DO apply here and are enforced per call.`
91779
91881
  );
91780
91882
  }
91883
+ function assertExistingProxyBillsUs(existing, requested) {
91884
+ const { listenPort } = requested;
91885
+ const describe = (mode) => mode === "api-key" ? "a BlockRun API key" : "a wallet";
91886
+ if (existing.authMode !== requested.authMode) {
91887
+ throw new Error(
91888
+ `Existing proxy on port ${listenPort} is authenticating with ${describe(existing.authMode)} but ${describe(requested.authMode)} was requested. Stop the existing proxy first or use a different port.`
91889
+ );
91890
+ }
91891
+ if (requested.authMode === "api-key") {
91892
+ if (existing.apiKeyLabel !== requested.apiKeyLabel) {
91893
+ throw new Error(
91894
+ `Existing proxy on port ${listenPort} is billing ${existing.apiKeyLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${requested.apiKeyLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
91895
+ );
91896
+ }
91897
+ return;
91898
+ }
91899
+ if (existing.paymentChain) {
91900
+ if (existing.paymentChain !== requested.paymentChain) {
91901
+ throw new Error(
91902
+ `Existing proxy on port ${listenPort} is using ${existing.paymentChain} but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
91903
+ );
91904
+ }
91905
+ } else if (requested.paymentChain !== "base") {
91906
+ console.warn(
91907
+ `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91908
+ );
91909
+ throw new Error(
91910
+ `Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
91911
+ );
91912
+ }
91913
+ }
91781
91914
  async function startProxy(options) {
91782
91915
  const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
91783
91916
  if (upstreamProxy) {
@@ -91814,32 +91947,29 @@ async function startProxy(options) {
91814
91947
  console.warn(`[ClawRouter] or run "npx @blockrun/clawrouter chain base" to switch to EVM.`);
91815
91948
  } else if (paymentChain === "solana") {
91816
91949
  console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
91950
+ } else {
91951
+ console.log(`[ClawRouter] Payment chain: Base (${apiBase})`);
91817
91952
  }
91818
91953
  void loadGatewayCatalog(apiBase, apiKey);
91819
91954
  const listenPort = options.port ?? getProxyPort();
91820
91955
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91956
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91821
91957
  if (existingProxy) {
91822
91958
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
91823
- if (existingProxy.authMode !== authMode) {
91824
- throw new Error(
91825
- `Existing proxy on port ${listenPort} is authenticating with ${existingProxy.authMode === "api-key" ? "a BlockRun API key" : "a wallet"} but ${authMode === "api-key" ? "an API key" : "a wallet"} was requested. Stop the existing proxy first or use a different port.`
91826
- );
91827
- }
91959
+ assertExistingProxyBillsUs(existingProxy, {
91960
+ listenPort,
91961
+ authMode,
91962
+ apiKeyLabel: ourApiKeyLabel,
91963
+ paymentChain
91964
+ });
91828
91965
  if (authMode === "api-key") {
91829
- const runningLabel = existingProxy.apiKeyLabel;
91830
- const ourLabel = maskApiKey(apiKey);
91831
- if (runningLabel !== ourLabel) {
91832
- throw new Error(
91833
- `Existing proxy on port ${listenPort} is billing ${runningLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${ourLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
91834
- );
91835
- }
91836
91966
  options.onReady?.(listenPort);
91837
91967
  return {
91838
91968
  port: listenPort,
91839
91969
  baseUrl: baseUrl2,
91840
91970
  walletAddress: "",
91841
91971
  authMode,
91842
- apiKeyLabel: ourLabel,
91972
+ apiKeyLabel: ourApiKeyLabel,
91843
91973
  balanceMonitor: new ApiKeyBalanceMonitor(),
91844
91974
  // No-op: we didn't start this proxy, so we shouldn't close it
91845
91975
  close: async () => {
@@ -91852,20 +91982,6 @@ async function startProxy(options) {
91852
91982
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91853
91983
  );
91854
91984
  }
91855
- if (existingProxy.paymentChain) {
91856
- if (existingProxy.paymentChain !== paymentChain) {
91857
- throw new Error(
91858
- `Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91859
- );
91860
- }
91861
- } else if (paymentChain !== "base") {
91862
- console.warn(
91863
- `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91864
- );
91865
- throw new Error(
91866
- `Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91867
- );
91868
- }
91869
91985
  let reuseSolanaAddress;
91870
91986
  if (solanaPrivateKeyBytes) {
91871
91987
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91935,7 +92051,16 @@ async function startProxy(options) {
91935
92051
  if (store) store.amountUsd = amountUsd;
91936
92052
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91937
92053
  });
91938
- const payFetch = authMode === "api-key" ? createApiKeyFetch(apiKey, fetch, apiBase) : createPayFetchWithPreAuth(fetch, x402, void 0, {
92054
+ const payFetch = authMode === "api-key" ? withSpendPolicy(
92055
+ createApiKeyFetch(apiKey, fetch, apiBase),
92056
+ options.spendControl ?? getSharedSpendControl(),
92057
+ () => {
92058
+ const store = paymentStore.getStore();
92059
+ const published = store?.estimatedUsd;
92060
+ if (store) store.estimatedUsd = void 0;
92061
+ return published;
92062
+ }
92063
+ ) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91939
92064
  skipPreAuth: paymentChain === "solana",
91940
92065
  // Per-request cost estimate so pre-auth is only reused when the cached
91941
92066
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -92000,6 +92125,7 @@ async function startProxy(options) {
92000
92125
  } else {
92001
92126
  response.wallet = account.address;
92002
92127
  response.paymentChain = paymentChain;
92128
+ response.gateway = apiBase;
92003
92129
  if (solanaAddress) {
92004
92130
  response.solana = solanaAddress;
92005
92131
  }
@@ -92318,6 +92444,7 @@ async function startProxy(options) {
92318
92444
  } catch {
92319
92445
  }
92320
92446
  try {
92447
+ publishDispatchCost(imgCost);
92321
92448
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92322
92449
  method: "POST",
92323
92450
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92516,6 +92643,7 @@ async function startProxy(options) {
92516
92643
  return;
92517
92644
  }
92518
92645
  try {
92646
+ publishDispatchCost(img2imgCost);
92519
92647
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92520
92648
  method: "POST",
92521
92649
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92619,6 +92747,7 @@ async function startProxy(options) {
92619
92747
  } catch {
92620
92748
  }
92621
92749
  try {
92750
+ publishDispatchCost(0.15);
92622
92751
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92623
92752
  method: "POST",
92624
92753
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92718,6 +92847,7 @@ async function startProxy(options) {
92718
92847
  } catch {
92719
92848
  }
92720
92849
  try {
92850
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92721
92851
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92722
92852
  method: "POST",
92723
92853
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93002,11 +93132,7 @@ async function startProxy(options) {
93002
93132
  const existingProxy2 = await checkExistingProxy(listenPort);
93003
93133
  if (existingProxy2) {
93004
93134
  console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
93005
- rejectAttempt({
93006
- code: "REUSE_EXISTING",
93007
- wallet: existingProxy2.wallet,
93008
- existingChain: existingProxy2.paymentChain
93009
- });
93135
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
93010
93136
  return;
93011
93137
  }
93012
93138
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -93038,21 +93164,21 @@ async function startProxy(options) {
93038
93164
  break;
93039
93165
  } catch (err) {
93040
93166
  const error = err;
93041
- if (error.code === "REUSE_EXISTING" && error.wallet) {
93042
- if (error.existingChain && error.existingChain !== paymentChain) {
93043
- throw new Error(
93044
- `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`,
93045
- { cause: err }
93046
- );
93047
- }
93167
+ if (error.code === "REUSE_EXISTING" && error.existing) {
93168
+ assertExistingProxyBillsUs(error.existing, {
93169
+ listenPort,
93170
+ authMode,
93171
+ apiKeyLabel: ourApiKeyLabel,
93172
+ paymentChain
93173
+ });
93048
93174
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
93049
93175
  options.onReady?.(listenPort);
93050
93176
  return {
93051
93177
  port: listenPort,
93052
93178
  baseUrl: baseUrl2,
93053
- walletAddress: error.wallet,
93179
+ walletAddress: error.existing.wallet,
93054
93180
  authMode,
93055
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
93181
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
93056
93182
  balanceMonitor,
93057
93183
  close: async () => {
93058
93184
  }
@@ -93517,6 +93643,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93517
93643
  size: imageSize,
93518
93644
  n: 1
93519
93645
  });
93646
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93520
93647
  const imageResponse = await payFetch(imageUpstreamUrl, {
93521
93648
  method: "POST",
93522
93649
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93798,6 +93925,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93798
93925
  size: img2imgSize,
93799
93926
  n: 1
93800
93927
  });
93928
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93801
93929
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93802
93930
  method: "POST",
93803
93931
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94627,6 +94755,8 @@ data: [DONE]
94627
94755
  throw abortError();
94628
94756
  }
94629
94757
  console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
94758
+ const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
94759
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94630
94760
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94631
94761
  const modelController = new AbortController();
94632
94762
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94726,6 +94856,7 @@ data: [DONE]
94726
94856
  );
94727
94857
  await new Promise((resolve) => setTimeout(resolve, 500));
94728
94858
  if (!globalController.signal.aborted) {
94859
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94729
94860
  const retryController = new AbortController();
94730
94861
  const retryTimeoutId = setTimeout(
94731
94862
  () => retryController.abort(),
@@ -94788,6 +94919,7 @@ data: [DONE]
94788
94919
  );
94789
94920
  await new Promise((resolve) => setTimeout(resolve, 200));
94790
94921
  if (!globalController.signal.aborted) {
94922
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94791
94923
  const retryController = new AbortController();
94792
94924
  const retryTimeoutId = setTimeout(
94793
94925
  () => retryController.abort(),
@@ -228566,6 +228698,19 @@ function isBlockrunWebSearchDisabled(config) {
228566
228698
  const cfg = config ?? {};
228567
228699
  return cfg.tools?.web?.search?.enabled === false;
228568
228700
  }
228701
+ function legacyPackageIsBlockRun() {
228702
+ for (const dir of [
228703
+ join18(homedir15(), ".openclaw", "extensions", "clawrouter"),
228704
+ join18(homedir15(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
228705
+ ]) {
228706
+ try {
228707
+ const metadata = JSON.parse(readTextFileSync(join18(dir, "package.json")));
228708
+ if (metadata.name === "@blockrun/clawrouter") return true;
228709
+ } catch {
228710
+ }
228711
+ }
228712
+ return false;
228713
+ }
228569
228714
  function injectModelsConfig(logger48, options = {}) {
228570
228715
  const configDir = join18(homedir15(), ".openclaw");
228571
228716
  const configPath = join18(configDir, "openclaw.json");
@@ -228729,9 +228874,9 @@ function injectModelsConfig(logger48, options = {}) {
228729
228874
  const legacyEntry = config.plugins?.entries?.clawrouter;
228730
228875
  const legacyEntryConfig = legacyEntry?.config;
228731
228876
  const legacyBlockRunInstall = Boolean(
228732
- legacyEntry && ["walletKey", "routing"].some(
228877
+ legacyEntry && (["walletKey", "routing"].some(
228733
228878
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228734
- )
228879
+ ) || legacyPackageIsBlockRun())
228735
228880
  );
228736
228881
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228737
228882
  needsWrite = true;
package/dist/index.d.ts CHANGED
@@ -834,6 +834,21 @@ declare class SpendControl {
834
834
  */
835
835
  reloadLimits(): void;
836
836
  check(estimatedCost: number, counterparty?: CounterpartyInfo): CheckResult;
837
+ /**
838
+ * The amount windows alone, with no counterparty to inspect.
839
+ *
840
+ * `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
841
+ * say nothing about how the money moves, so they are the part of the policy
842
+ * that means the same thing on the API-key rail as on the wallet rail — a
843
+ * daily cap is as sensible against account credit as against USDC, and both
844
+ * are read from the same `spending.json` (#329). The counterparty lists are
845
+ * the part that does NOT translate: `blockedPayees`, `allowedPayees`,
846
+ * `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
847
+ * asset, and on the key rail there is one counterparty and no on-chain asset.
848
+ * They are vacuous there rather than missing, which is why `check()` layers
849
+ * them on top of this instead of the other way round.
850
+ */
851
+ checkAmount(estimatedCost: number): CheckResult;
837
852
  record(amount: number, metadata?: {
838
853
  model?: string;
839
854
  action?: string;
@@ -1219,6 +1234,14 @@ type ProxyHandle = {
1219
1234
  balanceMonitor: AnyBalanceMonitor;
1220
1235
  close: () => Promise<void>;
1221
1236
  };
1237
+ /**
1238
+ * Start the local x402 proxy server.
1239
+ *
1240
+ * If a proxy is already running on the target port, reuses it instead of failing.
1241
+ * Port can be configured via BLOCKRUN_PROXY_PORT environment variable.
1242
+ *
1243
+ * Returns a handle with the assigned port, base URL, and a close function.
1244
+ */
1222
1245
  declare function startProxy(options: ProxyOptions): Promise<ProxyHandle>;
1223
1246
 
1224
1247
  /**
@@ -1778,23 +1801,6 @@ declare function buildPartnerTools(proxyBaseUrl: string): PartnerToolDefinition[
1778
1801
  * opt-in.
1779
1802
  */
1780
1803
  declare function isBlockrunWebSearchDisabled(config?: unknown): boolean;
1781
- /**
1782
- * Inject BlockRun models config into OpenClaw config file.
1783
- * This is required because registerProvider() alone doesn't make models available.
1784
- *
1785
- * CRITICAL: This function must be idempotent and handle ALL edge cases:
1786
- * - Config file doesn't exist (create it)
1787
- * - Config file exists but is empty/invalid (reinitialize)
1788
- * - blockrun provider exists but has undefined fields (fix them)
1789
- * - Config exists but uses old port/models (update them)
1790
- *
1791
- * This function is called on EVERY plugin load to ensure config is always correct.
1792
- *
1793
- * Also strips any previously managed `mcp.servers.blockrun` entry we wrote in
1794
- * older releases — ClawRouter no longer bundles the MCP bridge (the npx-spawned
1795
- * grandchildren were leaking). The scrub only removes entries matching the
1796
- * managed shape; user-defined `blockrun` MCP servers are left alone.
1797
- */
1798
1804
  declare function injectModelsConfig(logger: {
1799
1805
  info: (msg: string) => void;
1800
1806
  }, options?: {
package/dist/index.js CHANGED
@@ -34514,6 +34514,40 @@ function registerSpendPolicyHook(x402, control) {
34514
34514
  }
34515
34515
  });
34516
34516
  }
34517
+ function settledChargeUsd(response) {
34518
+ const raw = response.headers.get("x-blockrun-cost-usd");
34519
+ if (raw === null) return void 0;
34520
+ const trimmed = raw.trim();
34521
+ if (trimmed === "") return void 0;
34522
+ const value = Number(trimmed);
34523
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
34524
+ }
34525
+ function withSpendPolicy(inner, control, consumeEstimateUsd) {
34526
+ return async (input, init2) => {
34527
+ const published = consumeEstimateUsd();
34528
+ if (published === void 0 || !control.hasAmountLimits()) return inner(input, init2);
34529
+ const estimate = Number.isFinite(published) && published > 0 ? published : 0;
34530
+ const result = control.checkAmount(estimate);
34531
+ if (!result.allowed) {
34532
+ throw new SpendPolicyError(result.reason ?? "blocked by spend policy", {
34533
+ blockedBy: result.blockedBy
34534
+ });
34535
+ }
34536
+ const reservationId = control.hasAggregateLimits() ? control.reserve(estimate) : void 0;
34537
+ let response;
34538
+ try {
34539
+ response = await inner(input, init2);
34540
+ } catch (err) {
34541
+ if (reservationId !== void 0) control.releaseReservation(reservationId);
34542
+ throw err;
34543
+ }
34544
+ const settled = settledChargeUsd(response);
34545
+ const charged = response.ok ? settled ?? estimate : settled ?? 0;
34546
+ if (reservationId !== void 0) control.releaseReservation(reservationId);
34547
+ if (charged > 0) control.record(charged, { action: "BlockRun account charge" });
34548
+ return response;
34549
+ };
34550
+ }
34517
34551
  function formatDuration(seconds) {
34518
34552
  if (seconds < 60) {
34519
34553
  return `${seconds}s`;
@@ -34843,6 +34877,29 @@ var init_spend_control = __esm({
34843
34877
  };
34844
34878
  }
34845
34879
  }
34880
+ return this.checkAmount(estimatedCost2);
34881
+ }
34882
+ /**
34883
+ * The amount windows alone, with no counterparty to inspect.
34884
+ *
34885
+ * `perRequest` / `hourly` / `daily` / `session` are denominated in USD and
34886
+ * say nothing about how the money moves, so they are the part of the policy
34887
+ * that means the same thing on the API-key rail as on the wallet rail — a
34888
+ * daily cap is as sensible against account credit as against USDC, and both
34889
+ * are read from the same `spending.json` (#329). The counterparty lists are
34890
+ * the part that does NOT translate: `blockedPayees`, `allowedPayees`,
34891
+ * `blockedNetworks` and `allowedAssets` presuppose a payee, a network and an
34892
+ * asset, and on the key rail there is one counterparty and no on-chain asset.
34893
+ * They are vacuous there rather than missing, which is why `check()` layers
34894
+ * them on top of this instead of the other way round.
34895
+ */
34896
+ checkAmount(estimatedCost2) {
34897
+ if (this.policyFileBroken !== void 0) {
34898
+ return {
34899
+ allowed: false,
34900
+ reason: `Spend policy is unreadable, refusing all payments: ${this.policyFileBroken}`
34901
+ };
34902
+ }
34846
34903
  const now2 = this.now();
34847
34904
  if (this.limits.perRequest !== void 0) {
34848
34905
  if (estimatedCost2 > this.limits.perRequest) {
@@ -59733,6 +59790,28 @@ Then run: npx @blockrun/clawrouter`
59733
59790
  solanaPrivateKeyBytes
59734
59791
  };
59735
59792
  }
59793
+ async function warnIfLegacyWalletDiffers(coreAddress) {
59794
+ if (legacyWalletDivergenceWarned) return;
59795
+ let legacyAddress;
59796
+ try {
59797
+ const saved = await loadSavedWallet();
59798
+ if (!saved) return;
59799
+ legacyAddress = privateKeyToAccount(saved).address;
59800
+ } catch {
59801
+ return;
59802
+ }
59803
+ if (legacyAddress === coreAddress) return;
59804
+ legacyWalletDivergenceWarned = true;
59805
+ console.warn(
59806
+ `[ClawRouter] \u26A0 Two different wallets are configured. Paying from the BlockRun Core wallet ${coreAddress}.`
59807
+ );
59808
+ console.warn(
59809
+ `[ClawRouter] ClawRouter's older wallet ${legacyAddress} (${WALLET_FILE}) is NOT being used \u2014 if that is the funded one, requests will fail on an empty balance.`
59810
+ );
59811
+ console.warn(
59812
+ `[ClawRouter] To keep paying from it: export BLOCKRUN_WALLET_KEY=$(cat ${WALLET_FILE})`
59813
+ );
59814
+ }
59736
59815
  async function resolveExistingWalletKey() {
59737
59816
  const coreSolanaKey = await loadCoreSolanaKeyForSelectedChain();
59738
59817
  const envKey = process["env"].BLOCKRUN_WALLET_KEY;
@@ -59748,6 +59827,7 @@ async function resolveExistingWalletKey() {
59748
59827
  const core = await loadCoreWallet();
59749
59828
  if (core) {
59750
59829
  const account = privateKeyToAccount(core);
59830
+ await warnIfLegacyWalletDiffers(account.address);
59751
59831
  return {
59752
59832
  key: core,
59753
59833
  address: account.address,
@@ -59871,13 +59951,28 @@ async function savePaymentChain(chain3) {
59871
59951
  }
59872
59952
  async function loadPaymentChain() {
59873
59953
  const core = await readOptional(CORE_CHAIN_FILE);
59874
- if (core === "solana") return "solana";
59875
- if (core === "base") return "base";
59876
59954
  const legacy = await readOptional(CHAIN_FILE);
59955
+ if (core === "solana" || core === "base") {
59956
+ if ((legacy === "solana" || legacy === "base") && legacy !== core) {
59957
+ warnChainOverride(core, legacy);
59958
+ }
59959
+ return core;
59960
+ }
59877
59961
  if (legacy === "solana") return "solana";
59878
59962
  if (legacy === "base") return "base";
59879
59963
  return await hasExistingWallet() ? "base" : "solana";
59880
59964
  }
59965
+ function warnChainOverride(core, legacy) {
59966
+ if (chainOverrideWarned) return;
59967
+ chainOverrideWarned = true;
59968
+ console.warn(
59969
+ `[ClawRouter] \u26A0 Payment chain is ${core} (from ${CORE_CHAIN_FILE}), overriding this install's saved ${legacy}.`
59970
+ );
59971
+ console.warn(
59972
+ `[ClawRouter] A different chain means a different signer and gateway \u2014 funds on ${legacy} will not be spendable.`
59973
+ );
59974
+ console.warn(`[ClawRouter] To go back: npx @blockrun/clawrouter wallet ${legacy}`);
59975
+ }
59881
59976
  async function hasExistingWallet() {
59882
59977
  if (process["env"].BLOCKRUN_WALLET_KEY?.trim()) return true;
59883
59978
  const [core, legacy] = await Promise.all([
@@ -59891,7 +59986,7 @@ async function resolvePaymentChain() {
59891
59986
  if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
59892
59987
  return loadPaymentChain();
59893
59988
  }
59894
- var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE;
59989
+ var WALLET_DIR2, WALLET_FILE, MNEMONIC_FILE, CHAIN_FILE, CORE_WALLET_DIR, CORE_WALLET_FILE, CORE_SOLANA_WALLET_FILE, CORE_CHAIN_FILE, legacyWalletDivergenceWarned, chainOverrideWarned;
59895
59990
  var init_auth = __esm({
59896
59991
  "src/auth.ts"() {
59897
59992
  "use strict";
@@ -59907,6 +60002,8 @@ var init_auth = __esm({
59907
60002
  CORE_WALLET_FILE = join7(CORE_WALLET_DIR, ".session");
59908
60003
  CORE_SOLANA_WALLET_FILE = join7(CORE_WALLET_DIR, ".solana-session");
59909
60004
  CORE_CHAIN_FILE = join7(CORE_WALLET_DIR, ".chain");
60005
+ legacyWalletDivergenceWarned = false;
60006
+ chainOverrideWarned = false;
59910
60007
  }
59911
60008
  });
59912
60009
 
@@ -91361,6 +91458,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
91361
91458
  if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
91362
91459
  return estimate;
91363
91460
  }
91461
+ function publishDispatchCost(usd) {
91462
+ const store = paymentStore.getStore();
91463
+ if (store) store.estimatedUsd = Number.isFinite(usd) && usd > 0 ? usd : 0;
91464
+ }
91364
91465
  function noteRemainingCredit(response) {
91365
91466
  const remaining = gatewayRemainingCreditUsd(response);
91366
91467
  if (remaining === void 0) return;
@@ -91410,6 +91511,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91410
91511
  res.on("close", () => {
91411
91512
  if (!res.writableEnded) clientAbort.abort();
91412
91513
  });
91514
+ publishDispatchCost(0);
91413
91515
  const upstream = await payFetch(upstreamUrl, {
91414
91516
  method: req.method ?? "POST",
91415
91517
  headers,
@@ -91519,18 +91621,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
91519
91621
  let configured;
91520
91622
  try {
91521
91623
  const limits = (spendControl ?? new SpendControl()).getStatus().limits;
91522
- configured = ["perRequest", "hourly", "daily", "session"].filter((window2) => typeof limits[window2] === "number").map((window2) => `${window2}=$${limits[window2].toFixed(2)}`);
91624
+ configured = POLICY_LISTS.filter((list) => (limits[list]?.length ?? 0) > 0);
91523
91625
  } catch {
91524
91626
  return;
91525
91627
  }
91526
91628
  if (configured.length === 0) return;
91527
91629
  console.warn(
91528
- `[ClawRouter] \u26A0 Spend limits (${configured.join(", ")}) are NOT enforced in API-key mode \u2014 they gate x402 signing, and nothing is signed here.`
91630
+ `[ClawRouter] \u26A0 Counterparty policy (${configured.join(", ")}) does not apply in API-key mode \u2014 there is one counterparty and no on-chain asset.`
91529
91631
  );
91530
91632
  console.warn(
91531
- `[ClawRouter] Your BlockRun account balance is the cap instead. Use maxCostPerRun for a per-session limit that still applies.`
91633
+ `[ClawRouter] Your perRequest/hourly/daily/session limits DO apply here and are enforced per call.`
91532
91634
  );
91533
91635
  }
91636
+ function assertExistingProxyBillsUs(existing, requested) {
91637
+ const { listenPort } = requested;
91638
+ const describe = (mode) => mode === "api-key" ? "a BlockRun API key" : "a wallet";
91639
+ if (existing.authMode !== requested.authMode) {
91640
+ throw new Error(
91641
+ `Existing proxy on port ${listenPort} is authenticating with ${describe(existing.authMode)} but ${describe(requested.authMode)} was requested. Stop the existing proxy first or use a different port.`
91642
+ );
91643
+ }
91644
+ if (requested.authMode === "api-key") {
91645
+ if (existing.apiKeyLabel !== requested.apiKeyLabel) {
91646
+ throw new Error(
91647
+ `Existing proxy on port ${listenPort} is billing ${existing.apiKeyLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${requested.apiKeyLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
91648
+ );
91649
+ }
91650
+ return;
91651
+ }
91652
+ if (existing.paymentChain) {
91653
+ if (existing.paymentChain !== requested.paymentChain) {
91654
+ throw new Error(
91655
+ `Existing proxy on port ${listenPort} is using ${existing.paymentChain} but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
91656
+ );
91657
+ }
91658
+ } else if (requested.paymentChain !== "base") {
91659
+ console.warn(
91660
+ `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91661
+ );
91662
+ throw new Error(
91663
+ `Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${requested.paymentChain} was requested. Stop the existing proxy first or use a different port.`
91664
+ );
91665
+ }
91666
+ }
91534
91667
  async function startProxy(options) {
91535
91668
  const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
91536
91669
  if (upstreamProxy) {
@@ -91567,32 +91700,29 @@ async function startProxy(options) {
91567
91700
  console.warn(`[ClawRouter] or run "npx @blockrun/clawrouter chain base" to switch to EVM.`);
91568
91701
  } else if (paymentChain === "solana") {
91569
91702
  console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
91703
+ } else {
91704
+ console.log(`[ClawRouter] Payment chain: Base (${apiBase})`);
91570
91705
  }
91571
91706
  void loadGatewayCatalog(apiBase, apiKey);
91572
91707
  const listenPort = options.port ?? getProxyPort();
91573
91708
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91709
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91574
91710
  if (existingProxy) {
91575
91711
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
91576
- if (existingProxy.authMode !== authMode) {
91577
- throw new Error(
91578
- `Existing proxy on port ${listenPort} is authenticating with ${existingProxy.authMode === "api-key" ? "a BlockRun API key" : "a wallet"} but ${authMode === "api-key" ? "an API key" : "a wallet"} was requested. Stop the existing proxy first or use a different port.`
91579
- );
91580
- }
91712
+ assertExistingProxyBillsUs(existingProxy, {
91713
+ listenPort,
91714
+ authMode,
91715
+ apiKeyLabel: ourApiKeyLabel,
91716
+ paymentChain
91717
+ });
91581
91718
  if (authMode === "api-key") {
91582
- const runningLabel = existingProxy.apiKeyLabel;
91583
- const ourLabel = maskApiKey(apiKey);
91584
- if (runningLabel !== ourLabel) {
91585
- throw new Error(
91586
- `Existing proxy on port ${listenPort} is billing ${runningLabel ?? "an unidentified BlockRun account"}, but this process is configured for ${ourLabel}. Reusing it would charge the other account. Stop that proxy first, or use a different port.`
91587
- );
91588
- }
91589
91719
  options.onReady?.(listenPort);
91590
91720
  return {
91591
91721
  port: listenPort,
91592
91722
  baseUrl: baseUrl2,
91593
91723
  walletAddress: "",
91594
91724
  authMode,
91595
- apiKeyLabel: ourLabel,
91725
+ apiKeyLabel: ourApiKeyLabel,
91596
91726
  balanceMonitor: new ApiKeyBalanceMonitor(),
91597
91727
  // No-op: we didn't start this proxy, so we shouldn't close it
91598
91728
  close: async () => {
@@ -91605,20 +91735,6 @@ async function startProxy(options) {
91605
91735
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91606
91736
  );
91607
91737
  }
91608
- if (existingProxy.paymentChain) {
91609
- if (existingProxy.paymentChain !== paymentChain) {
91610
- throw new Error(
91611
- `Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91612
- );
91613
- }
91614
- } else if (paymentChain !== "base") {
91615
- console.warn(
91616
- `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91617
- );
91618
- throw new Error(
91619
- `Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91620
- );
91621
- }
91622
91738
  let reuseSolanaAddress;
91623
91739
  if (solanaPrivateKeyBytes) {
91624
91740
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91688,7 +91804,16 @@ async function startProxy(options) {
91688
91804
  if (store) store.amountUsd = amountUsd;
91689
91805
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91690
91806
  });
91691
- const payFetch = authMode === "api-key" ? createApiKeyFetch(apiKey, fetch, apiBase) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91807
+ const payFetch = authMode === "api-key" ? withSpendPolicy(
91808
+ createApiKeyFetch(apiKey, fetch, apiBase),
91809
+ options.spendControl ?? getSharedSpendControl(),
91810
+ () => {
91811
+ const store = paymentStore.getStore();
91812
+ const published = store?.estimatedUsd;
91813
+ if (store) store.estimatedUsd = void 0;
91814
+ return published;
91815
+ }
91816
+ ) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91692
91817
  skipPreAuth: paymentChain === "solana",
91693
91818
  // Per-request cost estimate so pre-auth is only reused when the cached
91694
91819
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -91753,6 +91878,7 @@ async function startProxy(options) {
91753
91878
  } else {
91754
91879
  response.wallet = account.address;
91755
91880
  response.paymentChain = paymentChain;
91881
+ response.gateway = apiBase;
91756
91882
  if (solanaAddress) {
91757
91883
  response.solana = solanaAddress;
91758
91884
  }
@@ -92071,6 +92197,7 @@ async function startProxy(options) {
92071
92197
  } catch {
92072
92198
  }
92073
92199
  try {
92200
+ publishDispatchCost(imgCost);
92074
92201
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92075
92202
  method: "POST",
92076
92203
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92269,6 +92396,7 @@ async function startProxy(options) {
92269
92396
  return;
92270
92397
  }
92271
92398
  try {
92399
+ publishDispatchCost(img2imgCost);
92272
92400
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92273
92401
  method: "POST",
92274
92402
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92372,6 +92500,7 @@ async function startProxy(options) {
92372
92500
  } catch {
92373
92501
  }
92374
92502
  try {
92503
+ publishDispatchCost(0.15);
92375
92504
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92376
92505
  method: "POST",
92377
92506
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92471,6 +92600,7 @@ async function startProxy(options) {
92471
92600
  } catch {
92472
92601
  }
92473
92602
  try {
92603
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92474
92604
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92475
92605
  method: "POST",
92476
92606
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92755,11 +92885,7 @@ async function startProxy(options) {
92755
92885
  const existingProxy2 = await checkExistingProxy(listenPort);
92756
92886
  if (existingProxy2) {
92757
92887
  console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
92758
- rejectAttempt({
92759
- code: "REUSE_EXISTING",
92760
- wallet: existingProxy2.wallet,
92761
- existingChain: existingProxy2.paymentChain
92762
- });
92888
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
92763
92889
  return;
92764
92890
  }
92765
92891
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -92791,21 +92917,21 @@ async function startProxy(options) {
92791
92917
  break;
92792
92918
  } catch (err) {
92793
92919
  const error = err;
92794
- if (error.code === "REUSE_EXISTING" && error.wallet) {
92795
- if (error.existingChain && error.existingChain !== paymentChain) {
92796
- throw new Error(
92797
- `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`,
92798
- { cause: err }
92799
- );
92800
- }
92920
+ if (error.code === "REUSE_EXISTING" && error.existing) {
92921
+ assertExistingProxyBillsUs(error.existing, {
92922
+ listenPort,
92923
+ authMode,
92924
+ apiKeyLabel: ourApiKeyLabel,
92925
+ paymentChain
92926
+ });
92801
92927
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
92802
92928
  options.onReady?.(listenPort);
92803
92929
  return {
92804
92930
  port: listenPort,
92805
92931
  baseUrl: baseUrl2,
92806
- walletAddress: error.wallet,
92932
+ walletAddress: error.existing.wallet,
92807
92933
  authMode,
92808
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
92934
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
92809
92935
  balanceMonitor,
92810
92936
  close: async () => {
92811
92937
  }
@@ -93270,6 +93396,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93270
93396
  size: imageSize,
93271
93397
  n: 1
93272
93398
  });
93399
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93273
93400
  const imageResponse = await payFetch(imageUpstreamUrl, {
93274
93401
  method: "POST",
93275
93402
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93551,6 +93678,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93551
93678
  size: img2imgSize,
93552
93679
  n: 1
93553
93680
  });
93681
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93554
93682
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93555
93683
  method: "POST",
93556
93684
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94380,6 +94508,8 @@ data: [DONE]
94380
94508
  throw abortError();
94381
94509
  }
94382
94510
  console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
94511
+ const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
94512
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94383
94513
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94384
94514
  const modelController = new AbortController();
94385
94515
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94479,6 +94609,7 @@ data: [DONE]
94479
94609
  );
94480
94610
  await new Promise((resolve) => setTimeout(resolve, 500));
94481
94611
  if (!globalController.signal.aborted) {
94612
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94482
94613
  const retryController = new AbortController();
94483
94614
  const retryTimeoutId = setTimeout(
94484
94615
  () => retryController.abort(),
@@ -94541,6 +94672,7 @@ data: [DONE]
94541
94672
  );
94542
94673
  await new Promise((resolve) => setTimeout(resolve, 200));
94543
94674
  if (!globalController.signal.aborted) {
94675
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94544
94676
  const retryController = new AbortController();
94545
94677
  const retryTimeoutId = setTimeout(
94546
94678
  () => retryController.abort(),
@@ -228050,6 +228182,19 @@ function isBlockrunWebSearchDisabled(config) {
228050
228182
  const cfg = config ?? {};
228051
228183
  return cfg.tools?.web?.search?.enabled === false;
228052
228184
  }
228185
+ function legacyPackageIsBlockRun() {
228186
+ for (const dir of [
228187
+ join17(homedir14(), ".openclaw", "extensions", "clawrouter"),
228188
+ join17(homedir14(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
228189
+ ]) {
228190
+ try {
228191
+ const metadata = JSON.parse(readTextFileSync(join17(dir, "package.json")));
228192
+ if (metadata.name === "@blockrun/clawrouter") return true;
228193
+ } catch {
228194
+ }
228195
+ }
228196
+ return false;
228197
+ }
228053
228198
  function injectModelsConfig(logger48, options = {}) {
228054
228199
  const configDir = join17(homedir14(), ".openclaw");
228055
228200
  const configPath = join17(configDir, "openclaw.json");
@@ -228213,9 +228358,9 @@ function injectModelsConfig(logger48, options = {}) {
228213
228358
  const legacyEntry = config.plugins?.entries?.clawrouter;
228214
228359
  const legacyEntryConfig = legacyEntry?.config;
228215
228360
  const legacyBlockRunInstall = Boolean(
228216
- legacyEntry && ["walletKey", "routing"].some(
228361
+ legacyEntry && (["walletKey", "routing"].some(
228217
228362
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228218
- )
228363
+ ) || legacyPackageIsBlockRun())
228219
228364
  );
228220
228365
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228221
228366
  needsWrite = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.273",
3
+ "version": "0.12.275",
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",
@@ -58,6 +58,7 @@
58
58
  "test:resilience:full": "npm run test:resilience:errors && npm run test:resilience:lifecycle && npm run test:resilience:stability:full",
59
59
  "test:e2e": "tsx test/test-e2e.ts",
60
60
  "test:e2e:tool-ids": "npm run build && tsx test/e2e-tool-id-sanitization.ts",
61
+ "test:e2e:openclaw-collision": "npm run build && tsx test/openclaw-plugin-collision-e2e.ts",
61
62
  "test:docker:install": "bash test/run-docker-test.sh",
62
63
  "test:docker:edge-cases": "bash test/docker/run-edge-cases.sh",
63
64
  "test:docker:integration": "docker compose -f test/integration/docker-compose.yml up --build --abort-on-container-exit --exit-code-from integration"