@blockrun/clawrouter 0.12.272 → 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
 
@@ -91138,7 +91235,12 @@ async function checkExistingProxy(port) {
91138
91235
  const data = await response.json();
91139
91236
  const authMode = data.authMode === "api-key" ? "api-key" : "wallet";
91140
91237
  if (data.status === "ok" && (data.wallet || authMode === "api-key")) {
91141
- return { wallet: data.wallet ?? "", paymentChain: data.paymentChain, authMode };
91238
+ return {
91239
+ wallet: data.wallet ?? "",
91240
+ paymentChain: data.paymentChain,
91241
+ authMode,
91242
+ apiKeyLabel: typeof data.apiKey === "string" ? data.apiKey : void 0
91243
+ };
91142
91244
  }
91143
91245
  }
91144
91246
  return void 0;
@@ -91603,6 +91705,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
91603
91705
  if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
91604
91706
  return estimate;
91605
91707
  }
91708
+ function publishDispatchCost(usd2) {
91709
+ const store = paymentStore.getStore();
91710
+ if (store) store.estimatedUsd = Number.isFinite(usd2) && usd2 > 0 ? usd2 : 0;
91711
+ }
91606
91712
  function noteRemainingCredit(response) {
91607
91713
  const remaining = gatewayRemainingCreditUsd(response);
91608
91714
  if (remaining === void 0) return;
@@ -91652,6 +91758,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91652
91758
  res.on("close", () => {
91653
91759
  if (!res.writableEnded) clientAbort.abort();
91654
91760
  });
91761
+ publishDispatchCost(0);
91655
91762
  const upstream = await payFetch(upstreamUrl, {
91656
91763
  method: req.method ?? "POST",
91657
91764
  headers,
@@ -91761,18 +91868,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
91761
91868
  let configured;
91762
91869
  try {
91763
91870
  const limits = (spendControl ?? new SpendControl()).getStatus().limits;
91764
- 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);
91765
91872
  } catch {
91766
91873
  return;
91767
91874
  }
91768
91875
  if (configured.length === 0) return;
91769
91876
  console.warn(
91770
- `[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.`
91771
91878
  );
91772
91879
  console.warn(
91773
- `[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.`
91774
91881
  );
91775
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
+ }
91776
91914
  async function startProxy(options) {
91777
91915
  const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
91778
91916
  if (upstreamProxy) {
@@ -91813,13 +91951,15 @@ async function startProxy(options) {
91813
91951
  void loadGatewayCatalog(apiBase, apiKey);
91814
91952
  const listenPort = options.port ?? getProxyPort();
91815
91953
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91954
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91816
91955
  if (existingProxy) {
91817
91956
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
91818
- if (existingProxy.authMode !== authMode) {
91819
- throw new Error(
91820
- `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.`
91821
- );
91822
- }
91957
+ assertExistingProxyBillsUs(existingProxy, {
91958
+ listenPort,
91959
+ authMode,
91960
+ apiKeyLabel: ourApiKeyLabel,
91961
+ paymentChain
91962
+ });
91823
91963
  if (authMode === "api-key") {
91824
91964
  options.onReady?.(listenPort);
91825
91965
  return {
@@ -91827,7 +91967,7 @@ async function startProxy(options) {
91827
91967
  baseUrl: baseUrl2,
91828
91968
  walletAddress: "",
91829
91969
  authMode,
91830
- apiKeyLabel: maskApiKey(apiKey),
91970
+ apiKeyLabel: ourApiKeyLabel,
91831
91971
  balanceMonitor: new ApiKeyBalanceMonitor(),
91832
91972
  // No-op: we didn't start this proxy, so we shouldn't close it
91833
91973
  close: async () => {
@@ -91840,20 +91980,6 @@ async function startProxy(options) {
91840
91980
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91841
91981
  );
91842
91982
  }
91843
- if (existingProxy.paymentChain) {
91844
- if (existingProxy.paymentChain !== paymentChain) {
91845
- throw new Error(
91846
- `Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91847
- );
91848
- }
91849
- } else if (paymentChain !== "base") {
91850
- console.warn(
91851
- `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91852
- );
91853
- throw new Error(
91854
- `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.`
91855
- );
91856
- }
91857
91983
  let reuseSolanaAddress;
91858
91984
  if (solanaPrivateKeyBytes) {
91859
91985
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91923,7 +92049,16 @@ async function startProxy(options) {
91923
92049
  if (store) store.amountUsd = amountUsd;
91924
92050
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91925
92051
  });
91926
- 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, {
91927
92062
  skipPreAuth: paymentChain === "solana",
91928
92063
  // Per-request cost estimate so pre-auth is only reused when the cached
91929
92064
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -92306,6 +92441,7 @@ async function startProxy(options) {
92306
92441
  } catch {
92307
92442
  }
92308
92443
  try {
92444
+ publishDispatchCost(imgCost);
92309
92445
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92310
92446
  method: "POST",
92311
92447
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92504,6 +92640,7 @@ async function startProxy(options) {
92504
92640
  return;
92505
92641
  }
92506
92642
  try {
92643
+ publishDispatchCost(img2imgCost);
92507
92644
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92508
92645
  method: "POST",
92509
92646
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92607,6 +92744,7 @@ async function startProxy(options) {
92607
92744
  } catch {
92608
92745
  }
92609
92746
  try {
92747
+ publishDispatchCost(0.15);
92610
92748
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92611
92749
  method: "POST",
92612
92750
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92706,6 +92844,7 @@ async function startProxy(options) {
92706
92844
  } catch {
92707
92845
  }
92708
92846
  try {
92847
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92709
92848
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92710
92849
  method: "POST",
92711
92850
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92990,11 +93129,7 @@ async function startProxy(options) {
92990
93129
  const existingProxy2 = await checkExistingProxy(listenPort);
92991
93130
  if (existingProxy2) {
92992
93131
  console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
92993
- rejectAttempt({
92994
- code: "REUSE_EXISTING",
92995
- wallet: existingProxy2.wallet,
92996
- existingChain: existingProxy2.paymentChain
92997
- });
93132
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
92998
93133
  return;
92999
93134
  }
93000
93135
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -93026,21 +93161,21 @@ async function startProxy(options) {
93026
93161
  break;
93027
93162
  } catch (err) {
93028
93163
  const error = err;
93029
- if (error.code === "REUSE_EXISTING" && error.wallet) {
93030
- if (error.existingChain && error.existingChain !== paymentChain) {
93031
- throw new Error(
93032
- `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`,
93033
- { cause: err }
93034
- );
93035
- }
93164
+ if (error.code === "REUSE_EXISTING" && error.existing) {
93165
+ assertExistingProxyBillsUs(error.existing, {
93166
+ listenPort,
93167
+ authMode,
93168
+ apiKeyLabel: ourApiKeyLabel,
93169
+ paymentChain
93170
+ });
93036
93171
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
93037
93172
  options.onReady?.(listenPort);
93038
93173
  return {
93039
93174
  port: listenPort,
93040
93175
  baseUrl: baseUrl2,
93041
- walletAddress: error.wallet,
93176
+ walletAddress: error.existing.wallet,
93042
93177
  authMode,
93043
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
93178
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
93044
93179
  balanceMonitor,
93045
93180
  close: async () => {
93046
93181
  }
@@ -93505,6 +93640,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93505
93640
  size: imageSize,
93506
93641
  n: 1
93507
93642
  });
93643
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93508
93644
  const imageResponse = await payFetch(imageUpstreamUrl, {
93509
93645
  method: "POST",
93510
93646
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93786,6 +93922,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93786
93922
  size: img2imgSize,
93787
93923
  n: 1
93788
93924
  });
93925
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93789
93926
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93790
93927
  method: "POST",
93791
93928
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94615,6 +94752,8 @@ data: [DONE]
94615
94752
  throw abortError();
94616
94753
  }
94617
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);
94618
94757
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94619
94758
  const modelController = new AbortController();
94620
94759
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94714,6 +94853,7 @@ data: [DONE]
94714
94853
  );
94715
94854
  await new Promise((resolve) => setTimeout(resolve, 500));
94716
94855
  if (!globalController.signal.aborted) {
94856
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94717
94857
  const retryController = new AbortController();
94718
94858
  const retryTimeoutId = setTimeout(
94719
94859
  () => retryController.abort(),
@@ -94776,6 +94916,7 @@ data: [DONE]
94776
94916
  );
94777
94917
  await new Promise((resolve) => setTimeout(resolve, 200));
94778
94918
  if (!globalController.signal.aborted) {
94919
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94779
94920
  const retryController = new AbortController();
94780
94921
  const retryTimeoutId = setTimeout(
94781
94922
  () => retryController.abort(),
@@ -228554,6 +228695,19 @@ function isBlockrunWebSearchDisabled(config) {
228554
228695
  const cfg = config ?? {};
228555
228696
  return cfg.tools?.web?.search?.enabled === false;
228556
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
+ }
228557
228711
  function injectModelsConfig(logger48, options = {}) {
228558
228712
  const configDir = join18(homedir15(), ".openclaw");
228559
228713
  const configPath = join18(configDir, "openclaw.json");
@@ -228717,9 +228871,9 @@ function injectModelsConfig(logger48, options = {}) {
228717
228871
  const legacyEntry = config.plugins?.entries?.clawrouter;
228718
228872
  const legacyEntryConfig = legacyEntry?.config;
228719
228873
  const legacyBlockRunInstall = Boolean(
228720
- legacyEntry && ["walletKey", "routing"].some(
228874
+ legacyEntry && (["walletKey", "routing"].some(
228721
228875
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228722
- )
228876
+ ) || legacyPackageIsBlockRun())
228723
228877
  );
228724
228878
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228725
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
 
@@ -90891,7 +90988,12 @@ async function checkExistingProxy(port) {
90891
90988
  const data = await response.json();
90892
90989
  const authMode = data.authMode === "api-key" ? "api-key" : "wallet";
90893
90990
  if (data.status === "ok" && (data.wallet || authMode === "api-key")) {
90894
- return { wallet: data.wallet ?? "", paymentChain: data.paymentChain, authMode };
90991
+ return {
90992
+ wallet: data.wallet ?? "",
90993
+ paymentChain: data.paymentChain,
90994
+ authMode,
90995
+ apiKeyLabel: typeof data.apiKey === "string" ? data.apiKey : void 0
90996
+ };
90895
90997
  }
90896
90998
  }
90897
90999
  return void 0;
@@ -91356,6 +91458,10 @@ function settledMediaCostUsd(upstream, body, x402AmountUsd, estimate) {
91356
91458
  if (typeof x402AmountUsd === "number" && x402AmountUsd > 0) return x402AmountUsd;
91357
91459
  return estimate;
91358
91460
  }
91461
+ function publishDispatchCost(usd) {
91462
+ const store = paymentStore.getStore();
91463
+ if (store) store.estimatedUsd = Number.isFinite(usd) && usd > 0 ? usd : 0;
91464
+ }
91359
91465
  function noteRemainingCredit(response) {
91360
91466
  const remaining = gatewayRemainingCreditUsd(response);
91361
91467
  if (remaining === void 0) return;
@@ -91405,6 +91511,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91405
91511
  res.on("close", () => {
91406
91512
  if (!res.writableEnded) clientAbort.abort();
91407
91513
  });
91514
+ publishDispatchCost(0);
91408
91515
  const upstream = await payFetch(upstreamUrl, {
91409
91516
  method: req.method ?? "POST",
91410
91517
  headers,
@@ -91514,18 +91621,49 @@ function warnIfSpendLimitsUnenforced(spendControl) {
91514
91621
  let configured;
91515
91622
  try {
91516
91623
  const limits = (spendControl ?? new SpendControl()).getStatus().limits;
91517
- 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);
91518
91625
  } catch {
91519
91626
  return;
91520
91627
  }
91521
91628
  if (configured.length === 0) return;
91522
91629
  console.warn(
91523
- `[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.`
91524
91631
  );
91525
91632
  console.warn(
91526
- `[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.`
91527
91634
  );
91528
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
+ }
91529
91667
  async function startProxy(options) {
91530
91668
  const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
91531
91669
  if (upstreamProxy) {
@@ -91566,13 +91704,15 @@ async function startProxy(options) {
91566
91704
  void loadGatewayCatalog(apiBase, apiKey);
91567
91705
  const listenPort = options.port ?? getProxyPort();
91568
91706
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91707
+ const ourApiKeyLabel = apiKey ? maskApiKey(apiKey) : void 0;
91569
91708
  if (existingProxy) {
91570
91709
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
91571
- if (existingProxy.authMode !== authMode) {
91572
- throw new Error(
91573
- `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.`
91574
- );
91575
- }
91710
+ assertExistingProxyBillsUs(existingProxy, {
91711
+ listenPort,
91712
+ authMode,
91713
+ apiKeyLabel: ourApiKeyLabel,
91714
+ paymentChain
91715
+ });
91576
91716
  if (authMode === "api-key") {
91577
91717
  options.onReady?.(listenPort);
91578
91718
  return {
@@ -91580,7 +91720,7 @@ async function startProxy(options) {
91580
91720
  baseUrl: baseUrl2,
91581
91721
  walletAddress: "",
91582
91722
  authMode,
91583
- apiKeyLabel: maskApiKey(apiKey),
91723
+ apiKeyLabel: ourApiKeyLabel,
91584
91724
  balanceMonitor: new ApiKeyBalanceMonitor(),
91585
91725
  // No-op: we didn't start this proxy, so we shouldn't close it
91586
91726
  close: async () => {
@@ -91593,20 +91733,6 @@ async function startProxy(options) {
91593
91733
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
91594
91734
  );
91595
91735
  }
91596
- if (existingProxy.paymentChain) {
91597
- if (existingProxy.paymentChain !== paymentChain) {
91598
- throw new Error(
91599
- `Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
91600
- );
91601
- }
91602
- } else if (paymentChain !== "base") {
91603
- console.warn(
91604
- `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
91605
- );
91606
- throw new Error(
91607
- `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.`
91608
- );
91609
- }
91610
91736
  let reuseSolanaAddress;
91611
91737
  if (solanaPrivateKeyBytes) {
91612
91738
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
@@ -91676,7 +91802,16 @@ async function startProxy(options) {
91676
91802
  if (store) store.amountUsd = amountUsd;
91677
91803
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91678
91804
  });
91679
- 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, {
91680
91815
  skipPreAuth: paymentChain === "solana",
91681
91816
  // Per-request cost estimate so pre-auth is only reused when the cached
91682
91817
  // payment still covers the (possibly larger) request — BlockRun prices per
@@ -92059,6 +92194,7 @@ async function startProxy(options) {
92059
92194
  } catch {
92060
92195
  }
92061
92196
  try {
92197
+ publishDispatchCost(imgCost);
92062
92198
  const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
92063
92199
  method: "POST",
92064
92200
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92257,6 +92393,7 @@ async function startProxy(options) {
92257
92393
  return;
92258
92394
  }
92259
92395
  try {
92396
+ publishDispatchCost(img2imgCost);
92260
92397
  let upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
92261
92398
  method: "POST",
92262
92399
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92360,6 +92497,7 @@ async function startProxy(options) {
92360
92497
  } catch {
92361
92498
  }
92362
92499
  try {
92500
+ publishDispatchCost(0.15);
92363
92501
  let upstream = await payFetch(`${apiBase}/v1/audio/generations`, {
92364
92502
  method: "POST",
92365
92503
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92459,6 +92597,7 @@ async function startProxy(options) {
92459
92597
  } catch {
92460
92598
  }
92461
92599
  try {
92600
+ publishDispatchCost(estimateVideoCost(videoModel, videoDuration, videoHasImageInput));
92462
92601
  const submitResp = await payFetch(`${apiBase}/v1/videos/generations`, {
92463
92602
  method: "POST",
92464
92603
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -92743,11 +92882,7 @@ async function startProxy(options) {
92743
92882
  const existingProxy2 = await checkExistingProxy(listenPort);
92744
92883
  if (existingProxy2) {
92745
92884
  console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
92746
- rejectAttempt({
92747
- code: "REUSE_EXISTING",
92748
- wallet: existingProxy2.wallet,
92749
- existingChain: existingProxy2.paymentChain
92750
- });
92885
+ rejectAttempt({ code: "REUSE_EXISTING", existing: existingProxy2 });
92751
92886
  return;
92752
92887
  }
92753
92888
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -92779,21 +92914,21 @@ async function startProxy(options) {
92779
92914
  break;
92780
92915
  } catch (err) {
92781
92916
  const error = err;
92782
- if (error.code === "REUSE_EXISTING" && error.wallet) {
92783
- if (error.existingChain && error.existingChain !== paymentChain) {
92784
- throw new Error(
92785
- `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`,
92786
- { cause: err }
92787
- );
92788
- }
92917
+ if (error.code === "REUSE_EXISTING" && error.existing) {
92918
+ assertExistingProxyBillsUs(error.existing, {
92919
+ listenPort,
92920
+ authMode,
92921
+ apiKeyLabel: ourApiKeyLabel,
92922
+ paymentChain
92923
+ });
92789
92924
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
92790
92925
  options.onReady?.(listenPort);
92791
92926
  return {
92792
92927
  port: listenPort,
92793
92928
  baseUrl: baseUrl2,
92794
- walletAddress: error.wallet,
92929
+ walletAddress: error.existing.wallet,
92795
92930
  authMode,
92796
- ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
92931
+ ...ourApiKeyLabel ? { apiKeyLabel: ourApiKeyLabel } : {},
92797
92932
  balanceMonitor,
92798
92933
  close: async () => {
92799
92934
  }
@@ -93258,6 +93393,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93258
93393
  size: imageSize,
93259
93394
  n: 1
93260
93395
  });
93396
+ publishDispatchCost(estimateImageCost(imageModel, imageSize, 1));
93261
93397
  const imageResponse = await payFetch(imageUpstreamUrl, {
93262
93398
  method: "POST",
93263
93399
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -93539,6 +93675,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
93539
93675
  size: img2imgSize,
93540
93676
  n: 1
93541
93677
  });
93678
+ publishDispatchCost(estimateImageCost(img2imgModel, img2imgSize, 1));
93542
93679
  const img2imgResponse = await payFetch(`${apiBase}/v1/images/image2image`, {
93543
93680
  method: "POST",
93544
93681
  headers: { "content-type": "application/json", "user-agent": USER_AGENT },
@@ -94368,6 +94505,8 @@ data: [DONE]
94368
94505
  throw abortError();
94369
94506
  }
94370
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);
94371
94510
  const perAttemptTimeoutMs = timeoutForModel(tryModel);
94372
94511
  const modelController = new AbortController();
94373
94512
  const modelTimeoutId = setTimeout(() => modelController.abort(), perAttemptTimeoutMs);
@@ -94467,6 +94606,7 @@ data: [DONE]
94467
94606
  );
94468
94607
  await new Promise((resolve) => setTimeout(resolve, 500));
94469
94608
  if (!globalController.signal.aborted) {
94609
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94470
94610
  const retryController = new AbortController();
94471
94611
  const retryTimeoutId = setTimeout(
94472
94612
  () => retryController.abort(),
@@ -94529,6 +94669,7 @@ data: [DONE]
94529
94669
  );
94530
94670
  await new Promise((resolve) => setTimeout(resolve, 200));
94531
94671
  if (!globalController.signal.aborted) {
94672
+ publishDispatchCost(attemptEst ? Number(attemptEst) / 1e6 : 0);
94532
94673
  const retryController = new AbortController();
94533
94674
  const retryTimeoutId = setTimeout(
94534
94675
  () => retryController.abort(),
@@ -228038,6 +228179,19 @@ function isBlockrunWebSearchDisabled(config) {
228038
228179
  const cfg = config ?? {};
228039
228180
  return cfg.tools?.web?.search?.enabled === false;
228040
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
+ }
228041
228195
  function injectModelsConfig(logger48, options = {}) {
228042
228196
  const configDir = join17(homedir14(), ".openclaw");
228043
228197
  const configPath = join17(configDir, "openclaw.json");
@@ -228201,9 +228355,9 @@ function injectModelsConfig(logger48, options = {}) {
228201
228355
  const legacyEntry = config.plugins?.entries?.clawrouter;
228202
228356
  const legacyEntryConfig = legacyEntry?.config;
228203
228357
  const legacyBlockRunInstall = Boolean(
228204
- legacyEntry && ["walletKey", "routing"].some(
228358
+ legacyEntry && (["walletKey", "routing"].some(
228205
228359
  (key2) => key2 in legacyEntry || (legacyEntryConfig ? key2 in legacyEntryConfig : false)
228206
- )
228360
+ ) || legacyPackageIsBlockRun())
228207
228361
  );
228208
228362
  if (legacyBlockRunInstall && prepareBlockRunPluginConfig(config, { legacyBlockRunInstall })) {
228209
228363
  needsWrite = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.272",
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",