@blockrun/clawrouter 0.12.273 → 0.12.274

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) {
@@ -91818,28 +91951,23 @@ async function startProxy(options) {
91818
91951
  void loadGatewayCatalog(apiBase, apiKey);
91819
91952
  const listenPort = options.port ?? getProxyPort();
91820
91953
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91954
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91821
91955
  if (existingProxy) {
91822
91956
  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
- }
91957
+ assertExistingProxyBillsUs(existingProxy, {
91958
+ listenPort,
91959
+ authMode,
91960
+ apiKeyLabel: ourApiKeyLabel,
91961
+ paymentChain
91962
+ });
91828
91963
  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
91964
  options.onReady?.(listenPort);
91837
91965
  return {
91838
91966
  port: listenPort,
91839
91967
  baseUrl: baseUrl2,
91840
91968
  walletAddress: "",
91841
91969
  authMode,
91842
- apiKeyLabel: ourLabel,
91970
+ apiKeyLabel: ourApiKeyLabel,
91843
91971
  balanceMonitor: new ApiKeyBalanceMonitor(),
91844
91972
  // No-op: we didn't start this proxy, so we shouldn't close it
91845
91973
  close: async () => {
@@ -91852,20 +91980,6 @@ async function startProxy(options) {
91852
91980
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91853
91981
  );
91854
91982
  }
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
91983
  let reuseSolanaAddress;
91870
91984
  if (solanaPrivateKeyBytes) {
91871
91985
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91935,7 +92049,16 @@ async function startProxy(options) {
91935
92049
  if (store) store.amountUsd = amountUsd;
91936
92050
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91937
92051
  });
91938
- const payFetch = authMode === "api-key" ? createApiKeyFetch(apiKey, fetch, apiBase) : createPayFetchWithPreAuth(fetch, x402, void 0, {
92052
+ const payFetch = authMode === "api-key" ? withSpendPolicy(
92053
+ createApiKeyFetch(apiKey, fetch, apiBase),
92054
+ options.spendControl ?? getSharedSpendControl(),
92055
+ () => {
92056
+ const store = paymentStore.getStore();
92057
+ const published = store?.estimatedUsd;
92058
+ if (store) store.estimatedUsd = void 0;
92059
+ return published;
92060
+ }
92061
+ ) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91939
92062
  skipPreAuth: paymentChain === "solana",
91940
92063
  // Per-request cost estimate so pre-auth is only reused when the cached
91941
92064
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -92318,6 +92441,7 @@ async function startProxy(options) {
92318
92441
  } catch {
92319
92442
  }
92320
92443
  try {
92444
+ publishDispatchCost(imgCost);
92321
92445
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92322
92446
  method: "POST",
92323
92447
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92516,6 +92640,7 @@ async function startProxy(options) {
92516
92640
  return;
92517
92641
  }
92518
92642
  try {
92643
+ publishDispatchCost(img2imgCost);
92519
92644
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92520
92645
  method: "POST",
92521
92646
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92619,6 +92744,7 @@ async function startProxy(options) {
92619
92744
  } catch {
92620
92745
  }
92621
92746
  try {
92747
+ publishDispatchCost(0.15);
92622
92748
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92623
92749
  method: "POST",
92624
92750
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92718,6 +92844,7 @@ async function startProxy(options) {
92718
92844
  } catch {
92719
92845
  }
92720
92846
  try {
92847
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92721
92848
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92722
92849
  method: "POST",
92723
92850
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93002,11 +93129,7 @@ async function startProxy(options) {
93002
93129
  const existingProxy2 = await checkExistingProxy(listenPort);
93003
93130
  if (existingProxy2) {
93004
93131
  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
- });
93132
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
93010
93133
  return;
93011
93134
  }
93012
93135
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -93038,21 +93161,21 @@ async function startProxy(options) {
93038
93161
  break;
93039
93162
  } catch (err) {
93040
93163
  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
- }
93164
+ if (error.code === "REUSE_EXISTING" && error.existing) {
93165
+ assertExistingProxyBillsUs(error.existing, {
93166
+ listenPort,
93167
+ authMode,
93168
+ apiKeyLabel: ourApiKeyLabel,
93169
+ paymentChain
93170
+ });
93048
93171
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
93049
93172
  options.onReady?.(listenPort);
93050
93173
  return {
93051
93174
  port: listenPort,
93052
93175
  baseUrl: baseUrl2,
93053
- walletAddress: error.wallet,
93176
+ walletAddress: error.existing.wallet,
93054
93177
  authMode,
93055
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
93178
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
93056
93179
  balanceMonitor,
93057
93180
  close: async () => {
93058
93181
  }
@@ -93517,6 +93640,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93517
93640
  size: imageSize,
93518
93641
  n: 1
93519
93642
  });
93643
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93520
93644
  const imageResponse = await payFetch(imageUpstreamUrl, {
93521
93645
  method: "POST",
93522
93646
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93798,6 +93922,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93798
93922
  size: img2imgSize,
93799
93923
  n: 1
93800
93924
  });
93925
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93801
93926
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93802
93927
  method: "POST",
93803
93928
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94627,6 +94752,8 @@ data: [DONE]
94627
94752
  throw abortError();
94628
94753
  }
94629
94754
  console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
94755
+ const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
94756
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94630
94757
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94631
94758
  const modelController = new AbortController();
94632
94759
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94726,6 +94853,7 @@ data: [DONE]
94726
94853
  );
94727
94854
  await new Promise((resolve) => setTimeout(resolve, 500));
94728
94855
  if (!globalController.signal.aborted) {
94856
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94729
94857
  const retryController = new AbortController();
94730
94858
  const retryTimeoutId = setTimeout(
94731
94859
  () => retryController.abort(),
@@ -94788,6 +94916,7 @@ data: [DONE]
94788
94916
  );
94789
94917
  await new Promise((resolve) => setTimeout(resolve, 200));
94790
94918
  if (!globalController.signal.aborted) {
94919
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94791
94920
  const retryController = new AbortController();
94792
94921
  const retryTimeoutId = setTimeout(
94793
94922
  () => retryController.abort(),
@@ -228566,6 +228695,19 @@ function isBlockrunWebSearchDisabled(config) {
228566
228695
  const cfg = config ?? {};
228567
228696
  return cfg.tools?.web?.search?.enabled === false;
228568
228697
  }
228698
+ function legacyPackageIsBlockRun() {
228699
+ for (const dir of [
228700
+ join18(homedir15(), ".openclaw", "extensions", "clawrouter"),
228701
+ join18(homedir15(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
228702
+ ]) {
228703
+ try {
228704
+ const metadata = JSON.parse(readTextFileSync(join18(dir, "package.json")));
228705
+ if (metadata.name === "@blockrun/clawrouter") return true;
228706
+ } catch {
228707
+ }
228708
+ }
228709
+ return false;
228710
+ }
228569
228711
  function injectModelsConfig(logger48, options = {}) {
228570
228712
  const configDir = join18(homedir15(), ".openclaw");
228571
228713
  const configPath = join18(configDir, "openclaw.json");
@@ -228729,9 +228871,9 @@ function injectModelsConfig(logger48, options = {}) {
228729
228871
  const legacyEntry = config.plugins?.entries?.clawrouter;
228730
228872
  const legacyEntryConfig = legacyEntry?.config;
228731
228873
  const legacyBlockRunInstall = Boolean(
228732
- legacyEntry && ["walletKey", "routing"].some(
228874
+ legacyEntry && (["walletKey", "routing"].some(
228733
228875
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228734
- )
228876
+ ) || legacyPackageIsBlockRun())
228735
228877
  );
228736
228878
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228737
228879
  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) {
@@ -91571,28 +91704,23 @@ async function startProxy(options) {
91571
91704
  void loadGatewayCatalog(apiBase, apiKey);
91572
91705
  const listenPort = options.port ?? getProxyPort();
91573
91706
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91707
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91574
91708
  if (existingProxy) {
91575
91709
  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
- }
91710
+ assertExistingProxyBillsUs(existingProxy, {
91711
+ listenPort,
91712
+ authMode,
91713
+ apiKeyLabel: ourApiKeyLabel,
91714
+ paymentChain
91715
+ });
91581
91716
  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
91717
  options.onReady?.(listenPort);
91590
91718
  return {
91591
91719
  port: listenPort,
91592
91720
  baseUrl: baseUrl2,
91593
91721
  walletAddress: "",
91594
91722
  authMode,
91595
- apiKeyLabel: ourLabel,
91723
+ apiKeyLabel: ourApiKeyLabel,
91596
91724
  balanceMonitor: new ApiKeyBalanceMonitor(),
91597
91725
  // No-op: we didn't start this proxy, so we shouldn't close it
91598
91726
  close: async () => {
@@ -91605,20 +91733,6 @@ async function startProxy(options) {
91605
91733
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91606
91734
  );
91607
91735
  }
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
91736
  let reuseSolanaAddress;
91623
91737
  if (solanaPrivateKeyBytes) {
91624
91738
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91688,7 +91802,16 @@ async function startProxy(options) {
91688
91802
  if (store) store.amountUsd = amountUsd;
91689
91803
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91690
91804
  });
91691
- const payFetch = authMode === "api-key" ? createApiKeyFetch(apiKey, fetch, apiBase) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91805
+ const payFetch = authMode === "api-key" ? withSpendPolicy(
91806
+ createApiKeyFetch(apiKey, fetch, apiBase),
91807
+ options.spendControl ?? getSharedSpendControl(),
91808
+ () => {
91809
+ const store = paymentStore.getStore();
91810
+ const published = store?.estimatedUsd;
91811
+ if (store) store.estimatedUsd = void 0;
91812
+ return published;
91813
+ }
91814
+ ) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91692
91815
  skipPreAuth: paymentChain === "solana",
91693
91816
  // Per-request cost estimate so pre-auth is only reused when the cached
91694
91817
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -92071,6 +92194,7 @@ async function startProxy(options) {
92071
92194
  } catch {
92072
92195
  }
92073
92196
  try {
92197
+ publishDispatchCost(imgCost);
92074
92198
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92075
92199
  method: "POST",
92076
92200
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92269,6 +92393,7 @@ async function startProxy(options) {
92269
92393
  return;
92270
92394
  }
92271
92395
  try {
92396
+ publishDispatchCost(img2imgCost);
92272
92397
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92273
92398
  method: "POST",
92274
92399
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92372,6 +92497,7 @@ async function startProxy(options) {
92372
92497
  } catch {
92373
92498
  }
92374
92499
  try {
92500
+ publishDispatchCost(0.15);
92375
92501
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92376
92502
  method: "POST",
92377
92503
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92471,6 +92597,7 @@ async function startProxy(options) {
92471
92597
  } catch {
92472
92598
  }
92473
92599
  try {
92600
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92474
92601
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92475
92602
  method: "POST",
92476
92603
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92755,11 +92882,7 @@ async function startProxy(options) {
92755
92882
  const existingProxy2 = await checkExistingProxy(listenPort);
92756
92883
  if (existingProxy2) {
92757
92884
  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
- });
92885
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
92763
92886
  return;
92764
92887
  }
92765
92888
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -92791,21 +92914,21 @@ async function startProxy(options) {
92791
92914
  break;
92792
92915
  } catch (err) {
92793
92916
  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
- }
92917
+ if (error.code === "REUSE_EXISTING" && error.existing) {
92918
+ assertExistingProxyBillsUs(error.existing, {
92919
+ listenPort,
92920
+ authMode,
92921
+ apiKeyLabel: ourApiKeyLabel,
92922
+ paymentChain
92923
+ });
92801
92924
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
92802
92925
  options.onReady?.(listenPort);
92803
92926
  return {
92804
92927
  port: listenPort,
92805
92928
  baseUrl: baseUrl2,
92806
- walletAddress: error.wallet,
92929
+ walletAddress: error.existing.wallet,
92807
92930
  authMode,
92808
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
92931
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
92809
92932
  balanceMonitor,
92810
92933
  close: async () => {
92811
92934
  }
@@ -93270,6 +93393,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93270
93393
  size: imageSize,
93271
93394
  n: 1
93272
93395
  });
93396
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93273
93397
  const imageResponse = await payFetch(imageUpstreamUrl, {
93274
93398
  method: "POST",
93275
93399
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93551,6 +93675,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93551
93675
  size: img2imgSize,
93552
93676
  n: 1
93553
93677
  });
93678
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93554
93679
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93555
93680
  method: "POST",
93556
93681
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94380,6 +94505,8 @@ data: [DONE]
94380
94505
  throw abortError();
94381
94506
  }
94382
94507
  console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
94508
+ const attemptEst = FREE_MODELS.has(tryModel) ? void 0 : estimateAmount(tryModel, body.length, maxTokens);
94509
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94383
94510
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94384
94511
  const modelController = new AbortController();
94385
94512
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94479,6 +94606,7 @@ data: [DONE]
94479
94606
  );
94480
94607
  await new Promise((resolve) => setTimeout(resolve, 500));
94481
94608
  if (!globalController.signal.aborted) {
94609
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94482
94610
  const retryController = new AbortController();
94483
94611
  const retryTimeoutId = setTimeout(
94484
94612
  () => retryController.abort(),
@@ -94541,6 +94669,7 @@ data: [DONE]
94541
94669
  );
94542
94670
  await new Promise((resolve) => setTimeout(resolve, 200));
94543
94671
  if (!globalController.signal.aborted) {
94672
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94544
94673
  const retryController = new AbortController();
94545
94674
  const retryTimeoutId = setTimeout(
94546
94675
  () => retryController.abort(),
@@ -228050,6 +228179,19 @@ function isBlockrunWebSearchDisabled(config) {
228050
228179
  const cfg = config ?? {};
228051
228180
  return cfg.tools?.web?.search?.enabled === false;
228052
228181
  }
228182
+ function legacyPackageIsBlockRun() {
228183
+ for (const dir of [
228184
+ join17(homedir14(), ".openclaw", "extensions", "clawrouter"),
228185
+ join17(homedir14(), ".openclaw", "npm", "node_modules", "@blockrun", "clawrouter")
228186
+ ]) {
228187
+ try {
228188
+ const metadata = JSON.parse(readTextFileSync(join17(dir, "package.json")));
228189
+ if (metadata.name === "@blockrun/clawrouter") return true;
228190
+ } catch {
228191
+ }
228192
+ }
228193
+ return false;
228194
+ }
228053
228195
  function injectModelsConfig(logger48, options = {}) {
228054
228196
  const configDir = join17(homedir14(), ".openclaw");
228055
228197
  const configPath = join17(configDir, "openclaw.json");
@@ -228213,9 +228355,9 @@ function injectModelsConfig(logger48, options = {}) {
228213
228355
  const legacyEntry = config.plugins?.entries?.clawrouter;
228214
228356
  const legacyEntryConfig = legacyEntry?.config;
228215
228357
  const legacyBlockRunInstall = Boolean(
228216
- legacyEntry && ["walletKey", "routing"].some(
228358
+ legacyEntry && (["walletKey", "routing"].some(
228217
228359
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228218
- )
228360
+ ) || legacyPackageIsBlockRun())
228219
228361
  );
228220
228362
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228221
228363
  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.274",
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",