@blockrun/clawrouter 0.12.268 → 0.12.269

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,23 @@ Or neither — <!-- br:models.free -->7<!-- /br:models.free --> models are free,
40
40
 
41
41
  ---
42
42
 
43
+ ## What BlockRun is
44
+
45
+ > **BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar.**
46
+
47
+ Not a seat, not a subscription, not a monthly minimum you keep paying while the agent idles. An agent asks for one thing — an answer, an image, a transcript, a price, a call placed — and pays for that one thing at the moment it happens. Nothing to sign up for, nothing to cancel.
48
+
49
+ | | |
50
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | **Every LLM** | <!-- br:models.chatVisible -->76<!-- /br:models.chatVisible --> chat models across 9 providers, behind one endpoint and one balance |
52
+ | **Every tool** | <!-- br:models.image -->9<!-- /br:models.image --> image models, <!-- br:models.video -->8<!-- /br:models.video --> video models, music, <!-- br:models.speech -->5<!-- /br:models.speech --> voices, img2img editing, and outbound phone calls that come back as transcripts |
53
+ | **Every data source** | web, news and neural search; prediction markets; live crypto and equity quotes; on-chain SQL over 100M+ labeled wallets; DEX routing; RPC across <!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains |
54
+ | **Best value per dollar** | <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions -->-dimension local routing sends each request to the cheapest model that can actually do it — <!-- br:savings.autoVsBaselinePct -->84<!-- /br:savings.autoVsBaselinePct -->% below pinning Claude Opus 5 on the same traffic, computed from a published workload mix rather than estimated |
55
+
56
+ **ClawRouter is how an agent reaches it.** BlockRun is the gateway and the billing; ClawRouter is the MIT-licensed router that runs on your machine, decides which model each request deserves, and settles the bill — a wallet signature in USDC, or a credit-card-funded API key. The rest of this README is about ClawRouter.
57
+
58
+ ---
59
+
43
60
  ## Why ClawRouter exists
44
61
 
45
62
  Every other LLM router was built for **human developers** — create an account, get an API key, pick a model from a dashboard, pay with a credit card.
@@ -868,6 +885,10 @@ Python plugin that wraps the ClawRouter proxy for `hermes-agent`. Same <!-- br:m
868
885
 
869
886
  ## Frequently Asked Questions
870
887
 
888
+ ### What is BlockRun?
889
+
890
+ BlockRun lets agents pay for the outcome — every LLM, tool and data source, best value per dollar. One endpoint and one balance covering <!-- br:models.chatVisible -->76<!-- /br:models.chatVisible --> chat models plus image, video, music and speech generation, search, market data and multi-chain RPC, billed per call with USDC over [x402](https://x402.org) or a credit-card-funded API key — no seat, no subscription, no minimum. ClawRouter is the open-source local router agents use to reach it.
891
+
871
892
  ### What is ClawRouter?
872
893
 
873
894
  ClawRouter is an open-source (MIT licensed) smart LLM router built for autonomous AI agents. It analyzes each request across <!-- br:clawrouter.dimensions -->15<!-- /br:clawrouter.dimensions --> dimensions and routes to the cheapest capable model in under 1ms, entirely locally — no external API calls needed for routing decisions.
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;
@@ -91588,7 +91662,7 @@ async function startProxy(options) {
91588
91662
  if (x402 && account) {
91589
91663
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
91590
91664
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
91591
- const spendControl = options.spendControl ?? new SpendControl();
91665
+ const spendControl = options.spendControl ?? getSharedSpendControl();
91592
91666
  registerSpendPolicyHook(x402, spendControl);
91593
91667
  registerExactEvmScheme(x402, { signer: evmSigner });
91594
91668
  }
@@ -91620,7 +91694,11 @@ async function startProxy(options) {
91620
91694
  // Per-request cost estimate so pre-auth is only reused when the cached
91621
91695
  // payment still covers the (possibly larger) request — BlockRun prices per
91622
91696
  // token, so one model can cost different amounts across requests.
91623
- estimateAmount
91697
+ estimateAmount,
91698
+ // Only the wallet rail settles per call. The API-key rail is billed
91699
+ // server-side against account credit, so there is no per-request
91700
+ // settlement for an observer to report.
91701
+ onPayment: options.onPayment
91624
91702
  });
91625
91703
  let balanceMonitor;
91626
91704
  if (options._balanceMonitorOverride) {
@@ -117892,9 +117970,7 @@ var init_client5 = __esm({
117892
117970
 
117893
117971
  // src/polymarket/spend-policy.ts
117894
117972
  function resolveSpendControl(deps) {
117895
- if (deps?.spendControl) return deps.spendControl;
117896
- defaultControl ??= new SpendControl();
117897
- return defaultControl;
117973
+ return deps?.spendControl ?? getSharedSpendControl();
117898
117974
  }
117899
117975
  function usdToMicros(usd) {
117900
117976
  return String(Math.ceil(usd * 1e6));
@@ -117911,7 +117987,6 @@ async function signUnderSpendPolicy(deps, quote, action, sign4) {
117911
117987
  throw err;
117912
117988
  }
117913
117989
  }
117914
- var defaultControl;
117915
117990
  var init_spend_policy = __esm({
117916
117991
  "src/polymarket/spend-policy.ts"() {
117917
117992
  "use strict";
@@ -227276,7 +227351,7 @@ var init_fund = __esm({
227276
227351
  });
227277
227352
 
227278
227353
  // src/polymarket/tool.ts
227279
- function buildPolymarketTool() {
227354
+ function buildPolymarketTool(deps) {
227280
227355
  return {
227281
227356
  name: "blockrun_polymarket",
227282
227357
  description: DESCRIPTION,
@@ -227361,26 +227436,32 @@ function buildPolymarketTool() {
227361
227436
  result = await runSetup({ confirm });
227362
227437
  break;
227363
227438
  case "fund":
227364
- result = await fundVault({
227365
- amount_usd: params.amount_usd,
227366
- confirm
227367
- });
227439
+ result = await fundVault(
227440
+ {
227441
+ amount_usd: params.amount_usd,
227442
+ confirm
227443
+ },
227444
+ deps
227445
+ );
227368
227446
  break;
227369
227447
  case "buy":
227370
227448
  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
- });
227449
+ result = await executeTrade(
227450
+ {
227451
+ action,
227452
+ token_id: params.token_id,
227453
+ condition_id: params.condition_id,
227454
+ outcome: params.outcome,
227455
+ price: params.price,
227456
+ size: params.size,
227457
+ amount_usd: params.amount_usd,
227458
+ order_type: params.order_type,
227459
+ expires_at: params.expires_at,
227460
+ post_only: params.post_only,
227461
+ confirm
227462
+ },
227463
+ deps
227464
+ );
227384
227465
  break;
227385
227466
  case "orders":
227386
227467
  result = await listOpenOrders({
@@ -227397,17 +227478,23 @@ function buildPolymarketTool() {
227397
227478
  result = await listPositions();
227398
227479
  break;
227399
227480
  case "redeem":
227400
- result = await redeemPosition({
227401
- condition_id: params.condition_id,
227402
- confirm
227403
- });
227481
+ result = await redeemPosition(
227482
+ {
227483
+ condition_id: params.condition_id,
227484
+ confirm
227485
+ },
227486
+ deps
227487
+ );
227404
227488
  break;
227405
227489
  case "withdraw":
227406
- result = await withdrawFunds({
227407
- amount_usd: params.amount_usd,
227408
- to_address: params.to_address,
227409
- confirm
227410
- });
227490
+ result = await withdrawFunds(
227491
+ {
227492
+ amount_usd: params.amount_usd,
227493
+ to_address: params.to_address,
227494
+ confirm
227495
+ },
227496
+ deps
227497
+ );
227411
227498
  break;
227412
227499
  default:
227413
227500
  throw new Error(`Unknown blockrun_polymarket action: ${action}`);
@@ -227823,6 +227910,7 @@ __export(index_exports, {
227823
227910
  SolanaBalanceMonitor: () => SolanaBalanceMonitor,
227824
227911
  SpendControl: () => SpendControl,
227825
227912
  SpendPolicyError: () => SpendPolicyError,
227913
+ UnreadableSpendPolicyError: () => UnreadableSpendPolicyError,
227826
227914
  VISIBLE_OPENCLAW_MODELS: () => VISIBLE_OPENCLAW_MODELS,
227827
227915
  blockrunProvider: () => blockrunProvider,
227828
227916
  buildImageGenerationProvider: () => buildImageGenerationProvider,
@@ -228403,12 +228491,18 @@ async function startProxyInBackground(api, startupGeneration) {
228403
228491
  `Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
228404
228492
  );
228405
228493
  }
228494
+ const sharedControl = getSharedSpendControl();
228495
+ sharedControl.reloadLimits();
228496
+ sharedControl.resetSession();
228406
228497
  const proxy = await startProxy({
228407
228498
  ...apiKey ? { apiKey } : { wallet },
228408
228499
  routingConfig,
228409
228500
  maxCostPerRunUsd,
228410
228501
  maxCostPerRunMode,
228411
- spendControl: liveSpendControl = new SpendControl(),
228502
+ // The process-wide ledger: the polymarket tool and the /policy command
228503
+ // registered below use the same instance, so hourly/daily/session windows
228504
+ // cover every surface and a /policy write reaches the live signer.
228505
+ spendControl: sharedControl,
228412
228506
  onReady: (port) => {
228413
228507
  api.logger.info(`BlockRun ${apiKey ? "API-key" : "x402"} proxy listening on port ${port}`);
228414
228508
  },
@@ -229151,7 +229245,7 @@ function createWalletCommand(api) {
229151
229245
  }
229152
229246
  };
229153
229247
  }
229154
- var activeProxyHandle, liveSpendControl, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
229248
+ var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
229155
229249
  var init_index = __esm({
229156
229250
  "src/index.ts"() {
229157
229251
  "use strict";
@@ -229170,10 +229264,10 @@ var init_index = __esm({
229170
229264
  init_stats();
229171
229265
  init_partners();
229172
229266
  init_tool();
229267
+ init_spend_control();
229173
229268
  init_stats2();
229174
229269
  init_exclude();
229175
229270
  init_policy();
229176
- init_spend_control();
229177
229271
  init_mcp_config();
229178
229272
  init_openclaw_plugin_config();
229179
229273
  init_openclaw_plugin_config();
@@ -229195,7 +229289,6 @@ var init_index = __esm({
229195
229289
  init_response_cache();
229196
229290
  init_partners();
229197
229291
  activeProxyHandle = null;
229198
- liveSpendControl = null;
229199
229292
  pendingConfiguredStartupApi = null;
229200
229293
  IMAGE_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "images");
229201
229294
  AUDIO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "audio");
@@ -229511,7 +229604,7 @@ ${errText}`
229511
229604
  api.registerCommand(createExcludeCommand());
229512
229605
  api.registerCommand(
229513
229606
  createPolicyCommand({
229514
- liveControl: () => activeProxyHandle ? liveSpendControl ?? void 0 : void 0
229607
+ liveControl: () => process.__clawrouterProxyStarted ? getSharedSpendControl() : void 0
229515
229608
  })
229516
229609
  );
229517
229610
  if (shouldLogRegistration) {
@@ -230089,7 +230182,7 @@ function createDoctorX402Client(opts) {
230089
230182
  const publicClient = createPublicClient({ chain: base, transport: http() });
230090
230183
  const evmSigner = toClientEvmSigner(account, publicClient);
230091
230184
  const x402 = new x402Client();
230092
- registerSpendPolicyHook(x402, opts.spendControl ?? new SpendControl());
230185
+ registerSpendPolicyHook(x402, opts.spendControl ?? getSharedSpendControl());
230093
230186
  registerExactEvmScheme(x402, { signer: evmSigner });
230094
230187
  return x402;
230095
230188
  }
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;
@@ -1814,4 +1841,4 @@ declare function buildImageGenerationProvider(): ImageGenerationProviderPlugin;
1814
1841
 
1815
1842
  declare const plugin: OpenClawPluginDefinition;
1816
1843
 
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 };
1844
+ 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 };
package/dist/index.js CHANGED
@@ -34446,6 +34446,11 @@ function canonicalLimits(limits) {
34446
34446
  Object.fromEntries(Object.entries(limits).sort(([a], [b]) => a < b ? -1 : 1))
34447
34447
  );
34448
34448
  }
34449
+ function getSharedSpendControl() {
34450
+ const host = sharedControlHost();
34451
+ host.__clawrouterSharedSpendControl ??= new SpendControl();
34452
+ return host.__clawrouterSharedSpendControl;
34453
+ }
34449
34454
  function parseQuotedAmountUsd(selected) {
34450
34455
  const raw = selected.amount ?? selected.maxAmountRequired;
34451
34456
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -34521,7 +34526,7 @@ function formatDuration(seconds) {
34521
34526
  return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
34522
34527
  }
34523
34528
  }
34524
- 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;
34529
+ 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;
34525
34530
  var init_spend_control = __esm({
34526
34531
  "src/spend-control.ts"() {
34527
34532
  "use strict";
@@ -34540,6 +34545,12 @@ var init_spend_control = __esm({
34540
34545
  ];
34541
34546
  ADDRESS_LISTS = ["allowedPayees", "blockedPayees", "allowedAssets"];
34542
34547
  EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
34548
+ UnreadableSpendPolicyError = class extends Error {
34549
+ constructor(cause) {
34550
+ super(`[ClawRouter] Failed to load spending data: ${cause}`);
34551
+ this.name = "UnreadableSpendPolicyError";
34552
+ }
34553
+ };
34543
34554
  MalformedSpendPolicyError = class extends Error {
34544
34555
  constructor(key2) {
34545
34556
  super(
@@ -34600,9 +34611,7 @@ var init_spend_control = __esm({
34600
34611
  if (err instanceof MalformedSpendPolicyError) {
34601
34612
  throw err;
34602
34613
  }
34603
- console.error(
34604
- `[ClawRouter] Failed to load spending data, starting fresh (any configured spend policy is NOT in effect until this file is repaired): ${err}`
34605
- );
34614
+ throw new UnreadableSpendPolicyError(err);
34606
34615
  }
34607
34616
  return null;
34608
34617
  }
@@ -34743,6 +34752,33 @@ var init_spend_control = __esm({
34743
34752
  getPolicyFileError() {
34744
34753
  return this.policyFileBroken;
34745
34754
  }
34755
+ /**
34756
+ * Re-read limits from storage, keeping this instance's history and open
34757
+ * reservations. Used on an in-process proxy restart so a hand-edit to
34758
+ * spending.json made while the process was running still applies, without
34759
+ * resetting the rolling windows. Fails closed exactly like the constructor:
34760
+ * a malformed file refuses every payment until repaired, and a repaired
34761
+ * file clears that refusal.
34762
+ */
34763
+ reloadLimits() {
34764
+ let data;
34765
+ try {
34766
+ data = this.storage.load();
34767
+ } catch (err) {
34768
+ if (err instanceof UnreadableSpendPolicyError) {
34769
+ console.error(`${err.message} \u2014 keeping the limits already in effect`);
34770
+ return;
34771
+ }
34772
+ if (!(err instanceof MalformedSpendPolicyError)) throw err;
34773
+ this.policyFileBroken = err.message;
34774
+ console.error(`[ClawRouter] ${err.message}`);
34775
+ return;
34776
+ }
34777
+ this.policyFileBroken = void 0;
34778
+ this.limits = data ? cloneLimits(data.limits) : {};
34779
+ this.diskLimits = cloneLimits(this.limits);
34780
+ this.limitsDirty = false;
34781
+ }
34746
34782
  check(estimatedCost2, counterparty) {
34747
34783
  if (this.policyFileBroken !== void 0) {
34748
34784
  return {
@@ -34987,6 +35023,16 @@ var init_spend_control = __esm({
34987
35023
  const records = [...this.history].reverse();
34988
35024
  return limit ? records.slice(0, limit) : records;
34989
35025
  }
35026
+ /**
35027
+ * Reset the session window. `sessionSpent`/`sessionCalls` are instance state
35028
+ * that is never persisted, so this used to happen implicitly: every
35029
+ * startProxy() built a fresh SpendControl. The process-wide ledger outlives
35030
+ * an in-process proxy restart, which would silently redefine `session` as
35031
+ * "since the gateway booted" — docs/configuration.md and the /policy help
35032
+ * both promise "session resets on restart". The restart path in index.ts
35033
+ * calls this to keep that promise. History and the rolling hourly/daily
35034
+ * windows are deliberately untouched.
35035
+ */
34990
35036
  resetSession() {
34991
35037
  this.sessionSpent = 0;
34992
35038
  this.sessionCalls = 0;
@@ -35008,10 +35054,13 @@ var init_spend_control = __esm({
35008
35054
  this.storage.saveLimits(cloneLimits(this.limits), cloneLimits(this.diskLimits));
35009
35055
  } catch (err) {
35010
35056
  if (err instanceof SpendPolicyConflictError) {
35011
- const current = this.storage.load();
35012
- this.limits = cloneLimits(current?.limits ?? {});
35013
- this.diskLimits = cloneLimits(this.limits);
35014
- this.limitsDirty = false;
35057
+ try {
35058
+ const current = this.storage.load();
35059
+ this.limits = cloneLimits(current?.limits ?? {});
35060
+ this.diskLimits = cloneLimits(this.limits);
35061
+ this.limitsDirty = false;
35062
+ } catch {
35063
+ }
35015
35064
  }
35016
35065
  throw err;
35017
35066
  }
@@ -35029,6 +35078,12 @@ var init_spend_control = __esm({
35029
35078
  try {
35030
35079
  data = this.storage.load();
35031
35080
  } catch (err) {
35081
+ if (err instanceof UnreadableSpendPolicyError) {
35082
+ console.error(
35083
+ `${err.message} \u2014 starting fresh (any configured spend policy is NOT in effect until this file is repaired)`
35084
+ );
35085
+ return;
35086
+ }
35032
35087
  if (!(err instanceof MalformedSpendPolicyError)) throw err;
35033
35088
  this.policyFileBroken = err.message;
35034
35089
  console.error(`[ClawRouter] ${err.message}`);
@@ -35045,6 +35100,7 @@ var init_spend_control = __esm({
35045
35100
  }
35046
35101
  }
35047
35102
  };
35103
+ sharedControlHost = () => process;
35048
35104
  SpendPolicyError = class extends Error {
35049
35105
  blockedBy;
35050
35106
  blockedByPolicy;
@@ -35082,6 +35138,21 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35082
35138
  }
35083
35139
  }
35084
35140
  const cacheKey2 = `${urlPath}:${requestModel}`;
35141
+ const notifyAcceptedPayment = (response2, payload2) => {
35142
+ const settled = response2.headers.has("payment-response") || response2.headers.has("x-payment-response");
35143
+ if (!settled || !options?.onPayment) return;
35144
+ try {
35145
+ options.onPayment({
35146
+ model: requestModel,
35147
+ amount: payload2.accepted.amount,
35148
+ network: payload2.accepted.network
35149
+ });
35150
+ } catch (error) {
35151
+ console.error(
35152
+ `[ClawRouter] onPayment callback failed: ${error instanceof Error ? error.message : String(error)}`
35153
+ );
35154
+ }
35155
+ };
35085
35156
  const estimateMicros = () => {
35086
35157
  if (!options?.estimateAmount || !requestModel) return void 0;
35087
35158
  const est = options.estimateAmount(requestModel, bodyLength, maxTokens);
@@ -35102,6 +35173,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35102
35173
  paymentInFlight = true;
35103
35174
  const response2 = await baseFetch(preAuthRequest);
35104
35175
  if (response2.status !== 402) {
35176
+ notifyAcceptedPayment(response2, payload2);
35105
35177
  return response2;
35106
35178
  }
35107
35179
  paymentInFlight = false;
@@ -35148,7 +35220,9 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
35148
35220
  for (const [key2, value] of Object.entries(paymentHeaders)) {
35149
35221
  clonedRequest.headers.set(key2, value);
35150
35222
  }
35151
- return baseFetch(clonedRequest);
35223
+ const paidResponse = await baseFetch(clonedRequest);
35224
+ notifyAcceptedPayment(paidResponse, payload);
35225
+ return paidResponse;
35152
35226
  };
35153
35227
  }
35154
35228
  var DEFAULT_TTL_MS;
@@ -91375,7 +91449,7 @@ async function startProxy(options) {
91375
91449
  if (x402 && account) {
91376
91450
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
91377
91451
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
91378
- const spendControl = options.spendControl ?? new SpendControl();
91452
+ const spendControl = options.spendControl ?? getSharedSpendControl();
91379
91453
  registerSpendPolicyHook(x402, spendControl);
91380
91454
  registerExactEvmScheme(x402, { signer: evmSigner });
91381
91455
  }
@@ -91407,7 +91481,11 @@ async function startProxy(options) {
91407
91481
  // Per-request cost estimate so pre-auth is only reused when the cached
91408
91482
  // payment still covers the (possibly larger) request — BlockRun prices per
91409
91483
  // token, so one model can cost different amounts across requests.
91410
- estimateAmount
91484
+ estimateAmount,
91485
+ // Only the wallet rail settles per call. The API-key rail is billed
91486
+ // server-side against account credit, so there is no per-request
91487
+ // settlement for an observer to report.
91488
+ onPayment: options.onPayment
91411
91489
  });
91412
91490
  let balanceMonitor;
91413
91491
  if (options._balanceMonitorOverride) {
@@ -117475,9 +117553,7 @@ var init_client5 = __esm({
117475
117553
 
117476
117554
  // src/polymarket/spend-policy.ts
117477
117555
  function resolveSpendControl(deps) {
117478
- if (deps?.spendControl) return deps.spendControl;
117479
- defaultControl ??= new SpendControl();
117480
- return defaultControl;
117556
+ return deps?.spendControl ?? getSharedSpendControl();
117481
117557
  }
117482
117558
  function usdToMicros(usd) {
117483
117559
  return String(Math.ceil(usd * 1e6));
@@ -117494,7 +117570,6 @@ async function signUnderSpendPolicy(deps, quote, action, sign4) {
117494
117570
  throw err;
117495
117571
  }
117496
117572
  }
117497
- var defaultControl;
117498
117573
  var init_spend_policy = __esm({
117499
117574
  "src/polymarket/spend-policy.ts"() {
117500
117575
  "use strict";
@@ -226859,7 +226934,7 @@ var init_fund = __esm({
226859
226934
  });
226860
226935
 
226861
226936
  // src/polymarket/tool.ts
226862
- function buildPolymarketTool() {
226937
+ function buildPolymarketTool(deps) {
226863
226938
  return {
226864
226939
  name: "blockrun_polymarket",
226865
226940
  description: DESCRIPTION,
@@ -226944,26 +227019,32 @@ function buildPolymarketTool() {
226944
227019
  result = await runSetup({ confirm });
226945
227020
  break;
226946
227021
  case "fund":
226947
- result = await fundVault({
226948
- amount_usd: params.amount_usd,
226949
- confirm
226950
- });
227022
+ result = await fundVault(
227023
+ {
227024
+ amount_usd: params.amount_usd,
227025
+ confirm
227026
+ },
227027
+ deps
227028
+ );
226951
227029
  break;
226952
227030
  case "buy":
226953
227031
  case "sell":
226954
- result = await executeTrade({
226955
- action,
226956
- token_id: params.token_id,
226957
- condition_id: params.condition_id,
226958
- outcome: params.outcome,
226959
- price: params.price,
226960
- size: params.size,
226961
- amount_usd: params.amount_usd,
226962
- order_type: params.order_type,
226963
- expires_at: params.expires_at,
226964
- post_only: params.post_only,
226965
- confirm
226966
- });
227032
+ result = await executeTrade(
227033
+ {
227034
+ action,
227035
+ token_id: params.token_id,
227036
+ condition_id: params.condition_id,
227037
+ outcome: params.outcome,
227038
+ price: params.price,
227039
+ size: params.size,
227040
+ amount_usd: params.amount_usd,
227041
+ order_type: params.order_type,
227042
+ expires_at: params.expires_at,
227043
+ post_only: params.post_only,
227044
+ confirm
227045
+ },
227046
+ deps
227047
+ );
226967
227048
  break;
226968
227049
  case "orders":
226969
227050
  result = await listOpenOrders({
@@ -226980,17 +227061,23 @@ function buildPolymarketTool() {
226980
227061
  result = await listPositions();
226981
227062
  break;
226982
227063
  case "redeem":
226983
- result = await redeemPosition({
226984
- condition_id: params.condition_id,
226985
- confirm
226986
- });
227064
+ result = await redeemPosition(
227065
+ {
227066
+ condition_id: params.condition_id,
227067
+ confirm
227068
+ },
227069
+ deps
227070
+ );
226987
227071
  break;
226988
227072
  case "withdraw":
226989
- result = await withdrawFunds({
226990
- amount_usd: params.amount_usd,
226991
- to_address: params.to_address,
226992
- confirm
226993
- });
227073
+ result = await withdrawFunds(
227074
+ {
227075
+ amount_usd: params.amount_usd,
227076
+ to_address: params.to_address,
227077
+ confirm
227078
+ },
227079
+ deps
227080
+ );
226994
227081
  break;
226995
227082
  default:
226996
227083
  throw new Error(`Unknown blockrun_polymarket action: ${action}`);
@@ -228110,12 +228197,18 @@ async function startProxyInBackground(api, startupGeneration) {
228110
228197
  `Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
228111
228198
  );
228112
228199
  }
228200
+ const sharedControl = getSharedSpendControl();
228201
+ sharedControl.reloadLimits();
228202
+ sharedControl.resetSession();
228113
228203
  const proxy = await startProxy({
228114
228204
  ...apiKey ? { apiKey } : { wallet },
228115
228205
  routingConfig,
228116
228206
  maxCostPerRunUsd,
228117
228207
  maxCostPerRunMode,
228118
- spendControl: liveSpendControl = new SpendControl(),
228208
+ // The process-wide ledger: the polymarket tool and the /policy command
228209
+ // registered below use the same instance, so hourly/daily/session windows
228210
+ // cover every surface and a /policy write reaches the live signer.
228211
+ spendControl: sharedControl,
228119
228212
  onReady: (port) => {
228120
228213
  api.logger.info(`BlockRun ${apiKey ? "API-key" : "x402"} proxy listening on port ${port}`);
228121
228214
  },
@@ -228858,7 +228951,7 @@ function createWalletCommand(api) {
228858
228951
  }
228859
228952
  };
228860
228953
  }
228861
- var activeProxyHandle, liveSpendControl, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
228954
+ var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
228862
228955
  var init_index = __esm({
228863
228956
  "src/index.ts"() {
228864
228957
  init_provider();
@@ -228876,10 +228969,10 @@ var init_index = __esm({
228876
228969
  init_stats();
228877
228970
  init_partners();
228878
228971
  init_tool();
228972
+ init_spend_control();
228879
228973
  init_stats2();
228880
228974
  init_exclude();
228881
228975
  init_policy();
228882
- init_spend_control();
228883
228976
  init_mcp_config();
228884
228977
  init_openclaw_plugin_config();
228885
228978
  init_openclaw_plugin_config();
@@ -228901,7 +228994,6 @@ var init_index = __esm({
228901
228994
  init_response_cache();
228902
228995
  init_partners();
228903
228996
  activeProxyHandle = null;
228904
- liveSpendControl = null;
228905
228997
  pendingConfiguredStartupApi = null;
228906
228998
  IMAGE_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "images");
228907
228999
  AUDIO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "audio");
@@ -229217,7 +229309,7 @@ ${errText}`
229217
229309
  api.registerCommand(createExcludeCommand());
229218
229310
  api.registerCommand(
229219
229311
  createPolicyCommand({
229220
- liveControl: () => activeProxyHandle ? liveSpendControl ?? void 0 : void 0
229312
+ liveControl: () => process.__clawrouterProxyStarted ? getSharedSpendControl() : void 0
229221
229313
  })
229222
229314
  );
229223
229315
  if (shouldLogRegistration) {
@@ -229415,6 +229507,7 @@ export {
229415
229507
  SolanaBalanceMonitor,
229416
229508
  SpendControl,
229417
229509
  SpendPolicyError,
229510
+ UnreadableSpendPolicyError,
229418
229511
  VISIBLE_OPENCLAW_MODELS,
229419
229512
  blockrunProvider,
229420
229513
  buildImageGenerationProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.268",
3
+ "version": "0.12.269",
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",