@blockrun/clawrouter 0.12.268 → 0.12.270

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
@@ -32310,6 +32310,11 @@ function canonicalLimits(limits) {
32310
32310
  Object.fromEntries(Object.entries(limits).sort(([a], [b]) => a < b ? -1 : 1))
32311
32311
  );
32312
32312
  }
32313
+ function getSharedSpendControl() {
32314
+ const host = sharedControlHost();
32315
+ host.__clawrouterSharedSpendControl ??= new SpendControl();
32316
+ return host.__clawrouterSharedSpendControl;
32317
+ }
32313
32318
  function parseQuotedAmountUsd(selected) {
32314
32319
  const raw = selected.amount ?? selected.maxAmountRequired;
32315
32320
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -32385,7 +32390,7 @@ function formatDuration(seconds) {
32385
32390
  return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
32386
32391
  }
32387
32392
  }
32388
- var WALLET_DIR, HOUR_MS, DAY_MS, CAIP2_BASE, CAIP2_SOLANA_MAINNET, PAYABLE_NETWORKS, POLICY_LISTS, ADDRESS_LISTS, EVM_ADDRESS, MalformedSpendPolicyError, SpendPolicyConflictError, FileSpendControlStorage, InMemorySpendControlStorage, RESERVATION_TTL_MS, SpendControl, SpendPolicyError, CANONICAL_AMOUNT;
32393
+ var WALLET_DIR, HOUR_MS, DAY_MS, CAIP2_BASE, CAIP2_SOLANA_MAINNET, PAYABLE_NETWORKS, POLICY_LISTS, ADDRESS_LISTS, EVM_ADDRESS, UnreadableSpendPolicyError, MalformedSpendPolicyError, SpendPolicyConflictError, FileSpendControlStorage, InMemorySpendControlStorage, RESERVATION_TTL_MS, SpendControl, sharedControlHost, SpendPolicyError, CANONICAL_AMOUNT;
32389
32394
  var init_spend_control = __esm({
32390
32395
  "src/spend-control.ts"() {
32391
32396
  "use strict";
@@ -32404,6 +32409,12 @@ var init_spend_control = __esm({
32404
32409
  ];
32405
32410
  ADDRESS_LISTS = ["allowedPayees", "blockedPayees", "allowedAssets"];
32406
32411
  EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
32412
+ UnreadableSpendPolicyError = class extends Error {
32413
+ constructor(cause) {
32414
+ super(`[ClawRouter] Failed to load spending data: ${cause}`);
32415
+ this.name = "UnreadableSpendPolicyError";
32416
+ }
32417
+ };
32407
32418
  MalformedSpendPolicyError = class extends Error {
32408
32419
  constructor(key2) {
32409
32420
  super(
@@ -32464,9 +32475,7 @@ var init_spend_control = __esm({
32464
32475
  if (err instanceof MalformedSpendPolicyError) {
32465
32476
  throw err;
32466
32477
  }
32467
- console.error(
32468
- `[ClawRouter] Failed to load spending data, starting fresh (any configured spend policy is NOT in effect until this file is repaired): ${err}`
32469
- );
32478
+ throw new UnreadableSpendPolicyError(err);
32470
32479
  }
32471
32480
  return null;
32472
32481
  }
@@ -32607,6 +32616,33 @@ var init_spend_control = __esm({
32607
32616
  getPolicyFileError() {
32608
32617
  return this.policyFileBroken;
32609
32618
  }
32619
+ /**
32620
+ * Re-read limits from storage, keeping this instance's history and open
32621
+ * reservations. Used on an in-process proxy restart so a hand-edit to
32622
+ * spending.json made while the process was running still applies, without
32623
+ * resetting the rolling windows. Fails closed exactly like the constructor:
32624
+ * a malformed file refuses every payment until repaired, and a repaired
32625
+ * file clears that refusal.
32626
+ */
32627
+ reloadLimits() {
32628
+ let data;
32629
+ try {
32630
+ data = this.storage.load();
32631
+ } catch (err) {
32632
+ if (err instanceof UnreadableSpendPolicyError) {
32633
+ console.error(`${err.message} \u2014 keeping the limits already in effect`);
32634
+ return;
32635
+ }
32636
+ if (!(err instanceof MalformedSpendPolicyError)) throw err;
32637
+ this.policyFileBroken = err.message;
32638
+ console.error(`[ClawRouter] ${err.message}`);
32639
+ return;
32640
+ }
32641
+ this.policyFileBroken = void 0;
32642
+ this.limits = data ? cloneLimits(data.limits) : {};
32643
+ this.diskLimits = cloneLimits(this.limits);
32644
+ this.limitsDirty = false;
32645
+ }
32610
32646
  check(estimatedCost2, counterparty) {
32611
32647
  if (this.policyFileBroken !== void 0) {
32612
32648
  return {
@@ -32851,6 +32887,16 @@ var init_spend_control = __esm({
32851
32887
  const records = [...this.history].reverse();
32852
32888
  return limit ? records.slice(0, limit) : records;
32853
32889
  }
32890
+ /**
32891
+ * Reset the session window. `sessionSpent`/`sessionCalls` are instance state
32892
+ * that is never persisted, so this used to happen implicitly: every
32893
+ * startProxy() built a fresh SpendControl. The process-wide ledger outlives
32894
+ * an in-process proxy restart, which would silently redefine `session` as
32895
+ * "since the gateway booted" — docs/configuration.md and the /policy help
32896
+ * both promise "session resets on restart". The restart path in index.ts
32897
+ * calls this to keep that promise. History and the rolling hourly/daily
32898
+ * windows are deliberately untouched.
32899
+ */
32854
32900
  resetSession() {
32855
32901
  this.sessionSpent = 0;
32856
32902
  this.sessionCalls = 0;
@@ -32872,10 +32918,13 @@ var init_spend_control = __esm({
32872
32918
  this.storage.saveLimits(cloneLimits(this.limits), cloneLimits(this.diskLimits));
32873
32919
  } catch (err) {
32874
32920
  if (err instanceof SpendPolicyConflictError) {
32875
- const current = this.storage.load();
32876
- this.limits = cloneLimits(current?.limits ?? {});
32877
- this.diskLimits = cloneLimits(this.limits);
32878
- this.limitsDirty = false;
32921
+ try {
32922
+ const current = this.storage.load();
32923
+ this.limits = cloneLimits(current?.limits ?? {});
32924
+ this.diskLimits = cloneLimits(this.limits);
32925
+ this.limitsDirty = false;
32926
+ } catch {
32927
+ }
32879
32928
  }
32880
32929
  throw err;
32881
32930
  }
@@ -32893,6 +32942,12 @@ var init_spend_control = __esm({
32893
32942
  try {
32894
32943
  data = this.storage.load();
32895
32944
  } catch (err) {
32945
+ if (err instanceof UnreadableSpendPolicyError) {
32946
+ console.error(
32947
+ `${err.message} \u2014 starting fresh (any configured spend policy is NOT in effect until this file is repaired)`
32948
+ );
32949
+ return;
32950
+ }
32896
32951
  if (!(err instanceof MalformedSpendPolicyError)) throw err;
32897
32952
  this.policyFileBroken = err.message;
32898
32953
  console.error(`[ClawRouter] ${err.message}`);
@@ -32909,6 +32964,7 @@ var init_spend_control = __esm({
32909
32964
  }
32910
32965
  }
32911
32966
  };
32967
+ sharedControlHost = () => process;
32912
32968
  SpendPolicyError = class extends Error {
32913
32969
  blockedBy;
32914
32970
  blockedByPolicy;
@@ -32946,6 +33002,21 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
32946
33002
  }
32947
33003
  }
32948
33004
  const cacheKey2 = `${urlPath}:${requestModel}`;
33005
+ const notifyAcceptedPayment = (response2, payload2) => {
33006
+ const settled = response2.headers.has("payment-response") || response2.headers.has("x-payment-response");
33007
+ if (!settled || !options?.onPayment) return;
33008
+ try {
33009
+ options.onPayment({
33010
+ model: requestModel,
33011
+ amount: payload2.accepted.amount,
33012
+ network: payload2.accepted.network
33013
+ });
33014
+ } catch (error) {
33015
+ console.error(
33016
+ `[ClawRouter] onPayment callback failed: ${error instanceof Error ? error.message : String(error)}`
33017
+ );
33018
+ }
33019
+ };
32949
33020
  const estimateMicros = () => {
32950
33021
  if (!options?.estimateAmount || !requestModel) return void 0;
32951
33022
  const est = options.estimateAmount(requestModel, bodyLength, maxTokens);
@@ -32966,6 +33037,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
32966
33037
  paymentInFlight = true;
32967
33038
  const response2 = await baseFetch(preAuthRequest);
32968
33039
  if (response2.status !== 402) {
33040
+ notifyAcceptedPayment(response2, payload2);
32969
33041
  return response2;
32970
33042
  }
32971
33043
  paymentInFlight = false;
@@ -33012,7 +33084,9 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33012
33084
  for (const [key2, value] of Object.entries(paymentHeaders)) {
33013
33085
  clonedRequest.headers.set(key2, value);
33014
33086
  }
33015
- return baseFetch(clonedRequest);
33087
+ const paidResponse = await baseFetch(clonedRequest);
33088
+ notifyAcceptedPayment(paidResponse, payload);
33089
+ return paidResponse;
33016
33090
  };
33017
33091
  }
33018
33092
  var DEFAULT_TTL_MS;
@@ -38467,8 +38541,8 @@ var init_models = __esm({
38467
38541
  id: "openai/gpt-5.6-sol",
38468
38542
  name: "GPT-5.6 Sol",
38469
38543
  version: "5.6",
38470
- inputPrice: 5,
38471
- outputPrice: 30,
38544
+ inputPrice: 4,
38545
+ outputPrice: 20,
38472
38546
  contextWindow: 105e4,
38473
38547
  maxOutput: 128e3,
38474
38548
  reasoning: true,
@@ -38509,8 +38583,8 @@ var init_models = __esm({
38509
38583
  id: "openai/gpt-5.6-sol-pro",
38510
38584
  name: "GPT-5.6 Sol Pro",
38511
38585
  version: "5.6",
38512
- inputPrice: 5,
38513
- outputPrice: 30,
38586
+ inputPrice: 4,
38587
+ outputPrice: 20,
38514
38588
  contextWindow: 105e4,
38515
38589
  maxOutput: 128e3,
38516
38590
  reasoning: true,
@@ -38948,8 +39022,8 @@ var init_models = __esm({
38948
39022
  id: "google/gemini-3.6-flash",
38949
39023
  name: "Gemini 3.6 Flash",
38950
39024
  version: "3.6",
38951
- inputPrice: 1.5,
38952
- outputPrice: 7.5,
39025
+ inputPrice: 0.75,
39026
+ outputPrice: 3.75,
38953
39027
  contextWindow: 1048576,
38954
39028
  maxOutput: 65536,
38955
39029
  reasoning: true,
@@ -39345,8 +39419,8 @@ var init_models = __esm({
39345
39419
  id: "xai/grok-4.5",
39346
39420
  name: "Grok 4.5",
39347
39421
  version: "4.5",
39348
- inputPrice: 2.5,
39349
- outputPrice: 9,
39422
+ inputPrice: 2,
39423
+ outputPrice: 6,
39350
39424
  contextWindow: 5e5,
39351
39425
  maxOutput: 16384,
39352
39426
  reasoning: true,
@@ -39360,8 +39434,8 @@ var init_models = __esm({
39360
39434
  id: "xai/grok-4.3",
39361
39435
  name: "Grok 4.3",
39362
39436
  version: "4.3",
39363
- inputPrice: 1.5,
39364
- outputPrice: 4,
39437
+ inputPrice: 1.25,
39438
+ outputPrice: 2.5,
39365
39439
  contextWindow: 1e6,
39366
39440
  maxOutput: 16384,
39367
39441
  reasoning: true,
@@ -39373,8 +39447,8 @@ var init_models = __esm({
39373
39447
  id: "xai/grok-build-0.1",
39374
39448
  name: "Grok Build 0.1",
39375
39449
  version: "0.1",
39376
- inputPrice: 1.5,
39377
- outputPrice: 3,
39450
+ inputPrice: 1,
39451
+ outputPrice: 2,
39378
39452
  contextWindow: 256e3,
39379
39453
  maxOutput: 16384,
39380
39454
  agentic: true,
@@ -59980,13 +60054,18 @@ async function loadPaymentChain() {
59980
60054
  const core = await readOptional(CORE_CHAIN_FILE);
59981
60055
  if (core === "solana") return "solana";
59982
60056
  if (core === "base") return "base";
59983
- try {
59984
- const content = (await readTextFile(CHAIN_FILE)).trim();
59985
- if (content === "solana") return "solana";
59986
- return "base";
59987
- } catch {
59988
- return "base";
59989
- }
60057
+ const legacy = await readOptional(CHAIN_FILE);
60058
+ if (legacy === "solana") return "solana";
60059
+ if (legacy === "base") return "base";
60060
+ return await hasExistingWallet() ? "base" : "solana";
60061
+ }
60062
+ async function hasExistingWallet() {
60063
+ if (process["env"].BLOCKRUN_WALLET_KEY?.trim()) return true;
60064
+ const [core, legacy] = await Promise.all([
60065
+ readOptional(CORE_WALLET_FILE),
60066
+ readOptional(WALLET_FILE)
60067
+ ]);
60068
+ return Boolean(core || legacy);
59990
60069
  }
59991
60070
  async function resolvePaymentChain() {
59992
60071
  if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "solana") return "solana";
@@ -60120,7 +60199,7 @@ var init_api_key = __esm({
60120
60199
  HINTS = {
60121
60200
  401: `Check BLOCKRUN_API_KEY, or mint a new key at ${PORTAL_KEYS_URL}.`,
60122
60201
  402: `Top up your BlockRun credit at ${PORTAL_CREDITS_URL}.`,
60123
- 404: `api.blockrun.ai does not serve this endpoint yet \u2014 it currently carries chat and text completions. For media and partner APIs, run ClawRouter in wallet mode (unset BLOCKRUN_API_KEY and run "clawrouter logout").`
60202
+ 404: `api.blockrun.ai does not serve this endpoint. Chat, Anthropic-shaped messages, images, speech, video and the partner APIs (Surf, Exa, prediction markets, phone lookup) all work on an API key. The wallet-only exceptions are the routes that bind a lease or a position to a payer address \u2014 buying/renewing/releasing phone numbers, and Polymarket trading \u2014 which need a wallet to own the thing being bought. For those, unset BLOCKRUN_API_KEY and run "clawrouter logout".`
60124
60203
  };
60125
60204
  }
60126
60205
  });
@@ -91588,7 +91667,7 @@ async function startProxy(options) {
91588
91667
  if (x402 && account) {
91589
91668
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
91590
91669
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
91591
- const spendControl = options.spendControl ?? new SpendControl();
91670
+ const spendControl = options.spendControl ?? getSharedSpendControl();
91592
91671
  registerSpendPolicyHook(x402, spendControl);
91593
91672
  registerExactEvmScheme(x402, { signer: evmSigner });
91594
91673
  }
@@ -91620,7 +91699,11 @@ async function startProxy(options) {
91620
91699
  // Per-request cost estimate so pre-auth is only reused when the cached
91621
91700
  // payment still covers the (possibly larger) request — BlockRun prices per
91622
91701
  // token, so one model can cost different amounts across requests.
91623
- estimateAmount
91702
+ estimateAmount,
91703
+ // Only the wallet rail settles per call. The API-key rail is billed
91704
+ // server-side against account credit, so there is no per-request
91705
+ // settlement for an observer to report.
91706
+ onPayment: options.onPayment
91624
91707
  });
91625
91708
  let balanceMonitor;
91626
91709
  if (options._balanceMonitorOverride) {
@@ -92500,7 +92583,8 @@ async function startProxy(options) {
92500
92583
  }
92501
92584
  return;
92502
92585
  }
92503
- if (req.url?.match(
92586
+ const isVerbatimGatewayPath = req.url === "/v1/messages" || (req.url?.startsWith("/v1/messages?") ?? false) || req.url === "/v1/audio/speech" || (req.url?.startsWith("/v1/audio/speech?") ?? false);
92587
+ if (isVerbatimGatewayPath || req.url?.match(
92504
92588
  /^\/v1\/(?:partner|pm|exa|modal|stocks|usstock|crypto|fx|commodity|phone|voice|surf)\//
92505
92589
  )) {
92506
92590
  try {
@@ -92532,6 +92616,19 @@ async function startProxy(options) {
92532
92616
  res.end(JSON.stringify({ error: "Not found" }));
92533
92617
  return;
92534
92618
  }
92619
+ if (!req.url.includes("/chat/completions")) {
92620
+ res.writeHead(404, { "Content-Type": "application/json" });
92621
+ res.end(
92622
+ JSON.stringify({
92623
+ error: {
92624
+ message: `ClawRouter does not proxy ${req.url}. It serves /v1/chat/completions, /v1/messages, /v1/models, /v1/images/generations, /v1/images/image2image, /v1/videos/generations, /v1/audio/generations, /v1/audio/speech, and the partner prefixes /v1/{partner,pm,exa,modal,stocks,usstock,crypto,fx,commodity,phone,voice,surf}/. If BlockRun serves this endpoint, call it directly against the gateway.`,
92625
+ type: "invalid_request_error",
92626
+ code: "unsupported_endpoint"
92627
+ }
92628
+ })
92629
+ );
92630
+ return;
92631
+ }
92535
92632
  try {
92536
92633
  await proxyRequest(
92537
92634
  req,
@@ -117892,9 +117989,7 @@ var init_client5 = __esm({
117892
117989
 
117893
117990
  // src/polymarket/spend-policy.ts
117894
117991
  function resolveSpendControl(deps) {
117895
- if (deps?.spendControl) return deps.spendControl;
117896
- defaultControl ??= new SpendControl();
117897
- return defaultControl;
117992
+ return deps?.spendControl ?? getSharedSpendControl();
117898
117993
  }
117899
117994
  function usdToMicros(usd) {
117900
117995
  return String(Math.ceil(usd * 1e6));
@@ -117911,7 +118006,6 @@ async function signUnderSpendPolicy(deps, quote, action, sign4) {
117911
118006
  throw err;
117912
118007
  }
117913
118008
  }
117914
- var defaultControl;
117915
118009
  var init_spend_policy = __esm({
117916
118010
  "src/polymarket/spend-policy.ts"() {
117917
118011
  "use strict";
@@ -227276,7 +227370,7 @@ var init_fund = __esm({
227276
227370
  });
227277
227371
 
227278
227372
  // src/polymarket/tool.ts
227279
- function buildPolymarketTool() {
227373
+ function buildPolymarketTool(deps) {
227280
227374
  return {
227281
227375
  name: "blockrun_polymarket",
227282
227376
  description: DESCRIPTION,
@@ -227361,26 +227455,32 @@ function buildPolymarketTool() {
227361
227455
  result = await runSetup({ confirm });
227362
227456
  break;
227363
227457
  case "fund":
227364
- result = await fundVault({
227365
- amount_usd: params.amount_usd,
227366
- confirm
227367
- });
227458
+ result = await fundVault(
227459
+ {
227460
+ amount_usd: params.amount_usd,
227461
+ confirm
227462
+ },
227463
+ deps
227464
+ );
227368
227465
  break;
227369
227466
  case "buy":
227370
227467
  case "sell":
227371
- result = await executeTrade({
227372
- action,
227373
- token_id: params.token_id,
227374
- condition_id: params.condition_id,
227375
- outcome: params.outcome,
227376
- price: params.price,
227377
- size: params.size,
227378
- amount_usd: params.amount_usd,
227379
- order_type: params.order_type,
227380
- expires_at: params.expires_at,
227381
- post_only: params.post_only,
227382
- confirm
227383
- });
227468
+ result = await executeTrade(
227469
+ {
227470
+ action,
227471
+ token_id: params.token_id,
227472
+ condition_id: params.condition_id,
227473
+ outcome: params.outcome,
227474
+ price: params.price,
227475
+ size: params.size,
227476
+ amount_usd: params.amount_usd,
227477
+ order_type: params.order_type,
227478
+ expires_at: params.expires_at,
227479
+ post_only: params.post_only,
227480
+ confirm
227481
+ },
227482
+ deps
227483
+ );
227384
227484
  break;
227385
227485
  case "orders":
227386
227486
  result = await listOpenOrders({
@@ -227397,17 +227497,23 @@ function buildPolymarketTool() {
227397
227497
  result = await listPositions();
227398
227498
  break;
227399
227499
  case "redeem":
227400
- result = await redeemPosition({
227401
- condition_id: params.condition_id,
227402
- confirm
227403
- });
227500
+ result = await redeemPosition(
227501
+ {
227502
+ condition_id: params.condition_id,
227503
+ confirm
227504
+ },
227505
+ deps
227506
+ );
227404
227507
  break;
227405
227508
  case "withdraw":
227406
- result = await withdrawFunds({
227407
- amount_usd: params.amount_usd,
227408
- to_address: params.to_address,
227409
- confirm
227410
- });
227509
+ result = await withdrawFunds(
227510
+ {
227511
+ amount_usd: params.amount_usd,
227512
+ to_address: params.to_address,
227513
+ confirm
227514
+ },
227515
+ deps
227516
+ );
227411
227517
  break;
227412
227518
  default:
227413
227519
  throw new Error(`Unknown blockrun_polymarket action: ${action}`);
@@ -227823,6 +227929,7 @@ __export(index_exports, {
227823
227929
  SolanaBalanceMonitor: () => SolanaBalanceMonitor,
227824
227930
  SpendControl: () => SpendControl,
227825
227931
  SpendPolicyError: () => SpendPolicyError,
227932
+ UnreadableSpendPolicyError: () => UnreadableSpendPolicyError,
227826
227933
  VISIBLE_OPENCLAW_MODELS: () => VISIBLE_OPENCLAW_MODELS,
227827
227934
  blockrunProvider: () => blockrunProvider,
227828
227935
  buildImageGenerationProvider: () => buildImageGenerationProvider,
@@ -228403,12 +228510,18 @@ async function startProxyInBackground(api, startupGeneration) {
228403
228510
  `Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
228404
228511
  );
228405
228512
  }
228513
+ const sharedControl = getSharedSpendControl();
228514
+ sharedControl.reloadLimits();
228515
+ sharedControl.resetSession();
228406
228516
  const proxy = await startProxy({
228407
228517
  ...apiKey ? { apiKey } : { wallet },
228408
228518
  routingConfig,
228409
228519
  maxCostPerRunUsd,
228410
228520
  maxCostPerRunMode,
228411
- spendControl: liveSpendControl = new SpendControl(),
228521
+ // The process-wide ledger: the polymarket tool and the /policy command
228522
+ // registered below use the same instance, so hourly/daily/session windows
228523
+ // cover every surface and a /policy write reaches the live signer.
228524
+ spendControl: sharedControl,
228412
228525
  onReady: (port) => {
228413
228526
  api.logger.info(`BlockRun ${apiKey ? "API-key" : "x402"} proxy listening on port ${port}`);
228414
228527
  },
@@ -229151,7 +229264,7 @@ function createWalletCommand(api) {
229151
229264
  }
229152
229265
  };
229153
229266
  }
229154
- var activeProxyHandle, liveSpendControl, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
229267
+ var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
229155
229268
  var init_index = __esm({
229156
229269
  "src/index.ts"() {
229157
229270
  "use strict";
@@ -229170,10 +229283,10 @@ var init_index = __esm({
229170
229283
  init_stats();
229171
229284
  init_partners();
229172
229285
  init_tool();
229286
+ init_spend_control();
229173
229287
  init_stats2();
229174
229288
  init_exclude();
229175
229289
  init_policy();
229176
- init_spend_control();
229177
229290
  init_mcp_config();
229178
229291
  init_openclaw_plugin_config();
229179
229292
  init_openclaw_plugin_config();
@@ -229195,7 +229308,6 @@ var init_index = __esm({
229195
229308
  init_response_cache();
229196
229309
  init_partners();
229197
229310
  activeProxyHandle = null;
229198
- liveSpendControl = null;
229199
229311
  pendingConfiguredStartupApi = null;
229200
229312
  IMAGE_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "images");
229201
229313
  AUDIO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "audio");
@@ -229511,7 +229623,7 @@ ${errText}`
229511
229623
  api.registerCommand(createExcludeCommand());
229512
229624
  api.registerCommand(
229513
229625
  createPolicyCommand({
229514
- liveControl: () => activeProxyHandle ? liveSpendControl ?? void 0 : void 0
229626
+ liveControl: () => process.__clawrouterProxyStarted ? getSharedSpendControl() : void 0
229515
229627
  })
229516
229628
  );
229517
229629
  if (shouldLogRegistration) {
@@ -229540,7 +229652,15 @@ ${errText}`
229540
229652
  });
229541
229653
  if (!isGatewayMode()) {
229542
229654
  if (shouldLogRegistration) {
229543
- resolveOrGenerateWalletKey().then(({ address: address2, source }) => {
229655
+ void (async () => {
229656
+ const keyResolution = await resolveApiKey().catch(() => void 0);
229657
+ if (keyResolution) {
229658
+ api.logger.info(
229659
+ `Using BlockRun API key ${maskApiKey(keyResolution.key)} (from ${keyResolution.source}) \u2014 billing account credit, no wallet`
229660
+ );
229661
+ return;
229662
+ }
229663
+ const { address: address2, source } = await resolveOrGenerateWalletKey();
229544
229664
  if (source === "generated") {
229545
229665
  api.logger.warn(`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
229546
229666
  api.logger.warn(` NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW!`);
@@ -229557,7 +229677,7 @@ ${errText}`
229557
229677
  } else {
229558
229678
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address2}`);
229559
229679
  }
229560
- }).catch((err) => {
229680
+ })().catch((err) => {
229561
229681
  api.logger.warn(
229562
229682
  `Failed to initialize wallet: ${err instanceof Error ? err.message : String(err)}`
229563
229683
  );
@@ -230089,7 +230209,7 @@ function createDoctorX402Client(opts) {
230089
230209
  const publicClient = createPublicClient({ chain: base, transport: http() });
230090
230210
  const evmSigner = toClientEvmSigner(account, publicClient);
230091
230211
  const x402 = new x402Client();
230092
- registerSpendPolicyHook(x402, opts.spendControl ?? new SpendControl());
230212
+ registerSpendPolicyHook(x402, opts.spendControl ?? getSharedSpendControl());
230093
230213
  registerExactEvmScheme(x402, { signer: evmSigner });
230094
230214
  return x402;
230095
230215
  }
package/dist/index.d.ts CHANGED
@@ -665,6 +665,14 @@ declare const PAYABLE_NETWORKS: readonly string[];
665
665
  * pay, which is the one direction this file must never fail in. Callers
666
666
  * classify on `instanceof`, not on the message text.
667
667
  */
668
+ /**
669
+ * spending.json exists but could not be read or parsed. Distinct from "no file
670
+ * yet" (load() returns null) so a reload can tell a failed read from an empty
671
+ * store and refuse to widen what the agent may pay.
672
+ */
673
+ declare class UnreadableSpendPolicyError extends Error {
674
+ constructor(cause: unknown);
675
+ }
668
676
  declare class MalformedSpendPolicyError extends Error {
669
677
  constructor(key: string);
670
678
  }
@@ -816,6 +824,15 @@ declare class SpendControl {
816
824
  * reporting a change as applied.
817
825
  */
818
826
  getPolicyFileError(): string | undefined;
827
+ /**
828
+ * Re-read limits from storage, keeping this instance's history and open
829
+ * reservations. Used on an in-process proxy restart so a hand-edit to
830
+ * spending.json made while the process was running still applies, without
831
+ * resetting the rolling windows. Fails closed exactly like the constructor:
832
+ * a malformed file refuses every payment until repaired, and a repaired
833
+ * file clears that refusal.
834
+ */
835
+ reloadLimits(): void;
819
836
  check(estimatedCost: number, counterparty?: CounterpartyInfo): CheckResult;
820
837
  record(amount: number, metadata?: {
821
838
  model?: string;
@@ -850,6 +867,16 @@ declare class SpendControl {
850
867
  getRemaining(window: "hourly" | "daily" | "session"): number | null;
851
868
  getStatus(): SpendingStatus;
852
869
  getHistory(limit?: number): SpendRecord[];
870
+ /**
871
+ * Reset the session window. `sessionSpent`/`sessionCalls` are instance state
872
+ * that is never persisted, so this used to happen implicitly: every
873
+ * startProxy() built a fresh SpendControl. The process-wide ledger outlives
874
+ * an in-process proxy restart, which would silently redefine `session` as
875
+ * "since the gateway booted" — docs/configuration.md and the /policy help
876
+ * both promise "session resets on restart". The restart path in index.ts
877
+ * calls this to keep that promise. History and the rolling hourly/daily
878
+ * windows are deliberately untouched.
879
+ */
853
880
  resetSession(): void;
854
881
  private cleanup;
855
882
  private save;
@@ -1221,13 +1248,29 @@ declare function setupSolana(): Promise<{
1221
1248
  declare function savePaymentChain(chain: "base" | "solana"): Promise<void>;
1222
1249
  /**
1223
1250
  * Load the persisted payment chain selection from disk.
1224
- * Returns "base" if no file exists or the file is invalid.
1225
- * New installs persist "solana" at wallet generation, so an absent file
1226
- * means a pre-existing install whose funds live on Base.
1251
+ *
1252
+ * Solana is the preferred rail, and since v0.12.246 wallet generation persists
1253
+ * `solana` outright — so a fresh install answers "solana" from its own chain
1254
+ * file, not from a default. This function only decides the case where no chain
1255
+ * file exists at all, and there the answer turns on whether a wallet already
1256
+ * does:
1257
+ *
1258
+ * no chain file, no wallet → "solana" (nothing to strand; prefer Solana)
1259
+ * no chain file, wallet on disk → "base" (a pre-v0.12.246 install)
1260
+ *
1261
+ * That second line is the one that must not move. An install predating the
1262
+ * Solana default has USDC sitting in its Base wallet and no Solana balance;
1263
+ * flipping it would point every request at a gateway its money is not on and
1264
+ * fail them all with an empty balance. The same reasoning covers a wallet
1265
+ * supplied through BLOCKRUN_WALLET_KEY, and a generated wallet whose Solana
1266
+ * derivation failed (no chain file is written in that case, on purpose — see
1267
+ * resolveOrGenerateWalletKey — and the wallet it did write keeps us on Base,
1268
+ * which is the only chain that proxy can actually sign for).
1227
1269
  */
1228
1270
  declare function loadPaymentChain(): Promise<"base" | "solana">;
1229
1271
  /**
1230
- * Resolve payment chain: env var first → persisted file second → default "base".
1272
+ * Resolve payment chain: env var first → persisted file second → the
1273
+ * fresh-install preference (Solana) last. See loadPaymentChain.
1231
1274
  */
1232
1275
  declare function resolvePaymentChain(): Promise<"base" | "solana">;
1233
1276
 
@@ -1814,4 +1857,4 @@ declare function buildImageGenerationProvider(): ImageGenerationProviderPlugin;
1814
1857
 
1815
1858
  declare const plugin: OpenClawPluginDefinition;
1816
1859
 
1817
- export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, BLOCKRUN_PLUGIN_ID, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, PAYABLE_NETWORKS, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
1860
+ export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, BLOCKRUN_PLUGIN_ID, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, PAYABLE_NETWORKS, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, UnreadableSpendPolicyError, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };