@blockrun/clawrouter 0.12.267 → 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/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;
@@ -40559,7 +40633,7 @@ var init_errors5 = __esm({
40559
40633
  });
40560
40634
 
40561
40635
  // src/balance.ts
40562
- var USDC_BASE, CACHE_TTL_MS, BALANCE_THRESHOLDS, BalanceMonitor;
40636
+ var USDC_BASE, CACHE_TTL_MS, BALANCE_THRESHOLDS, BalanceMonitor, ApiKeyBalanceMonitor;
40563
40637
  var init_balance = __esm({
40564
40638
  "src/balance.ts"() {
40565
40639
  "use strict";
@@ -40688,6 +40762,38 @@ var init_balance = __esm({
40688
40762
  };
40689
40763
  }
40690
40764
  };
40765
+ ApiKeyBalanceMonitor = class _ApiKeyBalanceMonitor {
40766
+ /** Large enough that every `balance >= estimatedCost` comparison passes. */
40767
+ static UNMETERED = BigInt(Number.MAX_SAFE_INTEGER);
40768
+ async checkBalance() {
40769
+ return {
40770
+ balance: _ApiKeyBalanceMonitor.UNMETERED,
40771
+ // Never a dollar figure: no caller may print this as if it were one.
40772
+ balanceUSD: "account credit",
40773
+ isLow: false,
40774
+ isEmpty: false,
40775
+ walletAddress: ""
40776
+ };
40777
+ }
40778
+ async checkSufficient() {
40779
+ return { sufficient: true, info: await this.checkBalance() };
40780
+ }
40781
+ /** No local cache to adjust — the server keeps the books. */
40782
+ deductEstimated() {
40783
+ }
40784
+ invalidate() {
40785
+ }
40786
+ async refresh() {
40787
+ return this.checkBalance();
40788
+ }
40789
+ formatUSDC(amountMicros) {
40790
+ return `$${(Number(amountMicros) / 1e6).toFixed(2)}`;
40791
+ }
40792
+ /** No wallet in this mode; callers render the account instead. */
40793
+ getWalletAddress() {
40794
+ return "";
40795
+ }
40796
+ };
40691
40797
  }
40692
40798
  });
40693
40799
 
@@ -59787,6 +59893,99 @@ var init_auth = __esm({
59787
59893
  }
59788
59894
  });
59789
59895
 
59896
+ // src/api-key.ts
59897
+ import { writeFile as writeFile2, mkdir as mkdir3, rm } from "fs/promises";
59898
+ import { join as join8 } from "path";
59899
+ import { homedir as homedir5 } from "os";
59900
+ function isValidApiKey(value) {
59901
+ if (typeof value !== "string") return false;
59902
+ return /^brk_[A-Za-z0-9_-]{8,}$/.test(value.trim());
59903
+ }
59904
+ function maskApiKey(key2) {
59905
+ const trimmed = key2.trim();
59906
+ if (trimmed.length <= 18) return `${trimmed.slice(0, 8)}\u2026`;
59907
+ return `${trimmed.slice(0, 14)}\u2026${trimmed.slice(-4)}`;
59908
+ }
59909
+ async function readOptional2(path5) {
59910
+ try {
59911
+ return (await readTextFile(path5)).trim() || void 0;
59912
+ } catch {
59913
+ return void 0;
59914
+ }
59915
+ }
59916
+ async function resolveApiKey() {
59917
+ const envKey = process["env"].BLOCKRUN_API_KEY?.trim();
59918
+ if (envKey) {
59919
+ if (isValidApiKey(envKey)) return { key: envKey, source: "env" };
59920
+ console.warn(
59921
+ `[ClawRouter] \u26A0 BLOCKRUN_API_KEY is set but does not look like a BlockRun key (expected brk_\u2026) \u2014 ignoring.`
59922
+ );
59923
+ }
59924
+ for (const [path5, source] of [
59925
+ [CORE_API_KEY_FILE, "core"],
59926
+ [API_KEY_FILE, "saved"]
59927
+ ]) {
59928
+ const stored = await readOptional2(path5);
59929
+ if (stored === void 0) continue;
59930
+ if (isValidApiKey(stored)) return { key: stored, source };
59931
+ console.warn(
59932
+ `[ClawRouter] \u26A0 ${path5} does not contain a BlockRun key (expected brk_\u2026) \u2014 ignoring.`
59933
+ );
59934
+ }
59935
+ return void 0;
59936
+ }
59937
+ function createApiKeyFetch(apiKey, baseFetch = fetch) {
59938
+ return async (input, init2) => {
59939
+ const headers = new Headers(init2?.headers);
59940
+ headers.set("authorization", `Bearer ${apiKey}`);
59941
+ headers.delete("x-payment");
59942
+ headers.delete("x-api-key");
59943
+ const response = await baseFetch(input, { ...init2, headers });
59944
+ return response.ok ? response : explainApiKeyFailure(response);
59945
+ };
59946
+ }
59947
+ async function explainApiKeyFailure(response) {
59948
+ const hint = HINTS[response.status];
59949
+ if (!hint) return response;
59950
+ if (!(response.headers.get("content-type") ?? "").includes("json")) return response;
59951
+ let parsed;
59952
+ try {
59953
+ parsed = await response.clone().json();
59954
+ } catch {
59955
+ return response;
59956
+ }
59957
+ if (!parsed?.error || typeof parsed.error.message !== "string") return response;
59958
+ if (response.status === 404 && !/unsupported endpoint/i.test(parsed.error.message)) {
59959
+ return response;
59960
+ }
59961
+ parsed.error.message = `${parsed.error.message} ${hint}`;
59962
+ const headers = new Headers(response.headers);
59963
+ headers.delete("content-length");
59964
+ return new Response(JSON.stringify(parsed), {
59965
+ status: response.status,
59966
+ statusText: response.statusText,
59967
+ headers
59968
+ });
59969
+ }
59970
+ var API_KEY_FILE, CORE_API_KEY_FILE, PORTAL_URL, PORTAL_KEYS_URL, PORTAL_CREDITS_URL, BLOCKRUN_API_KEY_API, HINTS;
59971
+ var init_api_key = __esm({
59972
+ "src/api-key.ts"() {
59973
+ "use strict";
59974
+ init_fs_read();
59975
+ API_KEY_FILE = join8(homedir5(), ".openclaw", "blockrun", "api-key");
59976
+ CORE_API_KEY_FILE = join8(homedir5(), ".blockrun", ".api-key");
59977
+ PORTAL_URL = "https://user.blockrun.ai";
59978
+ PORTAL_KEYS_URL = `${PORTAL_URL}/dashboard/keys`;
59979
+ PORTAL_CREDITS_URL = `${PORTAL_URL}/dashboard/credits`;
59980
+ BLOCKRUN_API_KEY_API = process["env"].BLOCKRUN_API_BASE_URL?.replace(/\/+$/, "") || "https://api.blockrun.ai";
59981
+ HINTS = {
59982
+ 401: `Check BLOCKRUN_API_KEY, or mint a new key at ${PORTAL_KEYS_URL}.`,
59983
+ 402: `Top up your BlockRun credit at ${PORTAL_CREDITS_URL}.`,
59984
+ 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").`
59985
+ };
59986
+ }
59987
+ });
59988
+
59790
59989
  // src/compression/types.ts
59791
59990
  var DEFAULT_COMPRESSION_CONFIG;
59792
59991
  var init_types2 = __esm({
@@ -60876,8 +61075,8 @@ var init_updater = __esm({
60876
61075
 
60877
61076
  // src/exclude-models.ts
60878
61077
  import { readFileSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, statSync } from "fs";
60879
- import { join as join8, dirname as dirname2 } from "path";
60880
- import { homedir as homedir5 } from "os";
61078
+ import { join as join9, dirname as dirname2 } from "path";
61079
+ import { homedir as homedir6 } from "os";
60881
61080
  function loadExcludeList(filePath = DEFAULT_FILE_PATH) {
60882
61081
  try {
60883
61082
  const mtimeMs = statSync(filePath).mtimeMs;
@@ -60926,7 +61125,7 @@ var init_exclude_models = __esm({
60926
61125
  "src/exclude-models.ts"() {
60927
61126
  "use strict";
60928
61127
  init_models();
60929
- DEFAULT_FILE_PATH = join8(homedir5(), ".openclaw", "blockrun", "exclude-models.json");
61128
+ DEFAULT_FILE_PATH = join9(homedir6(), ".openclaw", "blockrun", "exclude-models.json");
60930
61129
  loadCache = /* @__PURE__ */ new Map();
60931
61130
  }
60932
61131
  });
@@ -75935,7 +76134,7 @@ var require_snapshot_utils = __commonJS({
75935
76134
  var require_snapshot_recorder = __commonJS({
75936
76135
  "node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
75937
76136
  "use strict";
75938
- var { writeFile: writeFile3, readFile: readFile3, mkdir: mkdir5 } = __require("fs/promises");
76137
+ var { writeFile: writeFile4, readFile: readFile3, mkdir: mkdir6 } = __require("fs/promises");
75939
76138
  var { dirname: dirname4, resolve } = __require("path");
75940
76139
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
75941
76140
  var { InvalidArgumentError, UndiciError } = require_errors();
@@ -76182,12 +76381,12 @@ var require_snapshot_recorder = __commonJS({
76182
76381
  throw new InvalidArgumentError("Snapshot path is required");
76183
76382
  }
76184
76383
  const resolvedPath = resolve(path5);
76185
- await mkdir5(dirname4(resolvedPath), { recursive: true });
76384
+ await mkdir6(dirname4(resolvedPath), { recursive: true });
76186
76385
  const data = Array.from(this.#snapshots.entries()).map(([hash5, snapshot]) => ({
76187
76386
  hash: hash5,
76188
76387
  snapshot
76189
76388
  }));
76190
- await writeFile3(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
76389
+ await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
76191
76390
  }
76192
76391
  /**
76193
76392
  * Clears all recorded snapshots
@@ -88516,9 +88715,9 @@ var init_textual_tool_calls = __esm({
88516
88715
  });
88517
88716
 
88518
88717
  // src/response-store.ts
88519
- import { appendFile as appendFile2, mkdir as mkdir3, readFile, readdir as readdir2 } from "fs/promises";
88520
- import { homedir as homedir6 } from "os";
88521
- import { join as join9 } from "path";
88718
+ import { appendFile as appendFile2, mkdir as mkdir4, readFile, readdir as readdir2 } from "fs/promises";
88719
+ import { homedir as homedir7 } from "os";
88720
+ import { join as join10 } from "path";
88522
88721
  import { randomBytes as randomBytes7 } from "crypto";
88523
88722
  function isEnabled() {
88524
88723
  const v = process.env.BLOCKRUN_RESPONSE_STORE;
@@ -88526,12 +88725,12 @@ function isEnabled() {
88526
88725
  }
88527
88726
  async function ensureDir2() {
88528
88727
  if (dirReady2) return;
88529
- await mkdir3(STORE_DIR, { recursive: true });
88728
+ await mkdir4(STORE_DIR, { recursive: true });
88530
88729
  dirReady2 = true;
88531
88730
  }
88532
88731
  function dailyFile(date = /* @__PURE__ */ new Date()) {
88533
88732
  const iso = date.toISOString().slice(0, 10);
88534
- return join9(STORE_DIR, `responses-${iso}.jsonl`);
88733
+ return join10(STORE_DIR, `responses-${iso}.jsonl`);
88535
88734
  }
88536
88735
  function genId() {
88537
88736
  return `resp_${Date.now()}_${randomBytes7(3).toString("hex")}`;
@@ -88581,7 +88780,7 @@ async function listRecent(limit = 20, daysBack = 7) {
88581
88780
  d.setUTCDate(d.getUTCDate() - i);
88582
88781
  const iso = d.toISOString().slice(0, 10);
88583
88782
  const name = `responses-${iso}.jsonl`;
88584
- if (files.includes(name)) candidateFiles.push(join9(STORE_DIR, name));
88783
+ if (files.includes(name)) candidateFiles.push(join10(STORE_DIR, name));
88585
88784
  }
88586
88785
  const all3 = [];
88587
88786
  for (const f of candidateFiles) {
@@ -88610,7 +88809,7 @@ var STORE_DIR, dirReady2;
88610
88809
  var init_response_store = __esm({
88611
88810
  "src/response-store.ts"() {
88612
88811
  "use strict";
88613
- STORE_DIR = join9(homedir6(), ".openclaw", "blockrun", "responses");
88812
+ STORE_DIR = join10(homedir7(), ".openclaw", "blockrun", "responses");
88614
88813
  dirReady2 = false;
88615
88814
  }
88616
88815
  });
@@ -90261,15 +90460,18 @@ import { AsyncLocalStorage } from "async_hooks";
90261
90460
  import { createHmac } from "crypto";
90262
90461
  import { createServer } from "http";
90263
90462
  import { finished } from "stream";
90264
- import { homedir as homedir7 } from "os";
90265
- import { join as join10 } from "path";
90266
- import { mkdir as mkdir4, writeFile as writeFile2, readFile as readFile2, stat as fsStat } from "fs/promises";
90463
+ import { homedir as homedir8 } from "os";
90464
+ import { join as join11 } from "path";
90465
+ import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile2, stat as fsStat } from "fs/promises";
90267
90466
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
90268
- async function loadGatewayCatalog(apiBase) {
90467
+ async function loadGatewayCatalog(apiBase, apiKey) {
90269
90468
  try {
90270
90469
  const controller = new AbortController();
90271
90470
  const timer2 = setTimeout(() => controller.abort(), GATEWAY_CATALOG_TIMEOUT_MS);
90272
- const res = await fetch(`${apiBase}/v1/models`, { signal: controller.signal });
90471
+ const res = await fetch(`${apiBase}/v1/models`, {
90472
+ signal: controller.signal,
90473
+ ...apiKey ? { headers: { authorization: `Bearer ${apiKey}` } } : {}
90474
+ });
90273
90475
  clearTimeout(timer2);
90274
90476
  if (!res.ok) return;
90275
90477
  const body = await res.json();
@@ -90568,8 +90770,9 @@ async function checkExistingProxy(port) {
90568
90770
  clearTimeout(timeoutId);
90569
90771
  if (response.ok) {
90570
90772
  const data = await response.json();
90571
- if (data.status === "ok" && data.wallet) {
90572
- return { wallet: data.wallet, paymentChain: data.paymentChain };
90773
+ const authMode = data.authMode === "api-key" ? "api-key" : "wallet";
90774
+ if (data.status === "ok" && (data.wallet || authMode === "api-key")) {
90775
+ return { wallet: data.wallet ?? "", paymentChain: data.paymentChain, authMode };
90573
90776
  }
90574
90777
  }
90575
90778
  return void 0;
@@ -91077,7 +91280,7 @@ async function proxyPaidApiRequest(req, res, apiBase, payFetch, getActualPayment
91077
91280
  });
91078
91281
  }
91079
91282
  function readImageFileAsDataUri(filePath) {
91080
- const resolved = filePath.startsWith("~/") ? join10(homedir7(), filePath.slice(2)) : filePath;
91283
+ const resolved = filePath.startsWith("~/") ? join11(homedir8(), filePath.slice(2)) : filePath;
91081
91284
  if (!existsSync2(resolved)) {
91082
91285
  throw new Error(`Image file not found: ${resolved}`);
91083
91286
  }
@@ -91120,16 +91323,48 @@ async function uploadDataUriToHost(dataUri) {
91120
91323
  clearTimeout(uploadTimeout);
91121
91324
  }
91122
91325
  }
91326
+ function warnIfSpendLimitsUnenforced(spendControl) {
91327
+ let configured;
91328
+ try {
91329
+ const limits = (spendControl ?? new SpendControl()).getStatus().limits;
91330
+ configured = ["perRequest", "hourly", "daily", "session"].filter((window2) => typeof limits[window2] === "number").map((window2) => `${window2}=$${limits[window2].toFixed(2)}`);
91331
+ } catch {
91332
+ return;
91333
+ }
91334
+ if (configured.length === 0) return;
91335
+ console.warn(
91336
+ `[ClawRouter] \u26A0 Spend limits (${configured.join(", ")}) are NOT enforced in API-key mode \u2014 they gate x402 signing, and nothing is signed here.`
91337
+ );
91338
+ console.warn(
91339
+ `[ClawRouter] Your BlockRun account balance is the cap instead. Use maxCostPerRun for a per-session limit that still applies.`
91340
+ );
91341
+ }
91123
91342
  async function startProxy(options) {
91124
91343
  const upstreamProxy = await applyUpstreamProxy(options.upstreamProxy);
91125
91344
  if (upstreamProxy) {
91126
91345
  console.log(`[ClawRouter] Upstream proxy: ${upstreamProxy}`);
91127
91346
  }
91128
- const walletKey = typeof options.wallet === "string" ? options.wallet : options.wallet.key;
91129
- const solanaPrivateKeyBytes = typeof options.wallet === "string" ? void 0 : options.wallet.solanaPrivateKeyBytes;
91347
+ const apiKey = options.apiKey?.trim() || void 0;
91348
+ if (apiKey && !isValidApiKey(apiKey)) {
91349
+ throw new Error(
91350
+ `BlockRun API key is malformed (expected it to start with "brk_"). Mint one at https://user.blockrun.ai/dashboard/keys`
91351
+ );
91352
+ }
91353
+ const authMode = apiKey ? "api-key" : "wallet";
91354
+ if (!apiKey && !options.wallet) {
91355
+ throw new Error(
91356
+ `startProxy needs a credential: pass either a wallet (x402) or an apiKey (brk_\u2026, from https://user.blockrun.ai/dashboard/keys).`
91357
+ );
91358
+ }
91359
+ const walletKey = options.wallet === void 0 ? void 0 : typeof options.wallet === "string" ? options.wallet : options.wallet.key;
91360
+ const solanaPrivateKeyBytes = options.wallet === void 0 || typeof options.wallet === "string" ? void 0 : options.wallet.solanaPrivateKeyBytes;
91130
91361
  const paymentChain = options.paymentChain ?? await resolvePaymentChain();
91131
- const apiBase = options.apiBase ?? (paymentChain === "solana" && solanaPrivateKeyBytes ? BLOCKRUN_SOLANA_API : BLOCKRUN_API);
91132
- if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
91362
+ const apiBase = options.apiBase ?? (authMode === "api-key" ? BLOCKRUN_API_KEY_API : paymentChain === "solana" && solanaPrivateKeyBytes ? BLOCKRUN_SOLANA_API : BLOCKRUN_API);
91363
+ if (authMode === "api-key") {
91364
+ console.log(`[ClawRouter] Auth: BlockRun API key ${maskApiKey(apiKey)} (${apiBase})`);
91365
+ console.log(`[ClawRouter] Billing: account credit \u2014 top up at ${PORTAL_CREDITS_URL}`);
91366
+ warnIfSpendLimitsUnenforced(options.spendControl);
91367
+ } else if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
91133
91368
  console.warn(
91134
91369
  `[ClawRouter] \u26A0 Payment chain is Solana but no mnemonic found \u2014 falling back to Base (EVM).`
91135
91370
  );
@@ -91140,12 +91375,31 @@ async function startProxy(options) {
91140
91375
  } else if (paymentChain === "solana") {
91141
91376
  console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
91142
91377
  }
91143
- void loadGatewayCatalog(apiBase);
91378
+ void loadGatewayCatalog(apiBase, apiKey);
91144
91379
  const listenPort = options.port ?? getProxyPort();
91145
91380
  const existingProxy = options.allowExistingProxy === false ? void 0 : await checkExistingProxy(listenPort);
91146
91381
  if (existingProxy) {
91147
- const account2 = privateKeyToAccount(walletKey);
91148
91382
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
91383
+ if (existingProxy.authMode !== authMode) {
91384
+ throw new Error(
91385
+ `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.`
91386
+ );
91387
+ }
91388
+ if (authMode === "api-key") {
91389
+ options.onReady?.(listenPort);
91390
+ return {
91391
+ port: listenPort,
91392
+ baseUrl: baseUrl2,
91393
+ walletAddress: "",
91394
+ authMode,
91395
+ apiKeyLabel: maskApiKey(apiKey),
91396
+ balanceMonitor: new ApiKeyBalanceMonitor(),
91397
+ // No-op: we didn't start this proxy, so we shouldn't close it
91398
+ close: async () => {
91399
+ }
91400
+ };
91401
+ }
91402
+ const account2 = privateKeyToAccount(walletKey);
91149
91403
  if (existingProxy.wallet !== account2.address) {
91150
91404
  console.warn(
91151
91405
  `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
@@ -91184,20 +91438,23 @@ async function startProxy(options) {
91184
91438
  baseUrl: baseUrl2,
91185
91439
  walletAddress: existingProxy.wallet,
91186
91440
  solanaAddress: reuseSolanaAddress,
91441
+ authMode,
91187
91442
  balanceMonitor: balanceMonitor2,
91188
91443
  close: async () => {
91189
91444
  }
91190
91445
  };
91191
91446
  }
91192
- const account = privateKeyToAccount(walletKey);
91193
- const evmPublicClient = createPublicClient({ chain: base, transport: http() });
91194
- const evmSigner = toClientEvmSigner(account, evmPublicClient);
91195
- const x402 = new x402Client();
91196
- const spendControl = options.spendControl ?? new SpendControl();
91197
- registerSpendPolicyHook(x402, spendControl);
91198
- registerExactEvmScheme(x402, { signer: evmSigner });
91447
+ const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
91448
+ const x402 = authMode === "wallet" ? new x402Client() : void 0;
91449
+ if (x402 && account) {
91450
+ const evmPublicClient = createPublicClient({ chain: base, transport: http() });
91451
+ const evmSigner = toClientEvmSigner(account, evmPublicClient);
91452
+ const spendControl = options.spendControl ?? getSharedSpendControl();
91453
+ registerSpendPolicyHook(x402, spendControl);
91454
+ registerExactEvmScheme(x402, { signer: evmSigner });
91455
+ }
91199
91456
  let solanaAddress;
91200
- if (solanaPrivateKeyBytes) {
91457
+ if (x402 && solanaPrivateKeyBytes) {
91201
91458
  const { registerExactSvmScheme: registerExactSvmScheme2 } = await Promise.resolve().then(() => (init_client3(), client_exports));
91202
91459
  const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await Promise.resolve().then(() => (init_index_node37(), index_node_exports));
91203
91460
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes2(solanaPrivateKeyBytes);
@@ -91205,12 +91462,12 @@ async function startProxy(options) {
91205
91462
  registerExactSvmScheme2(x402, { signer: solanaSigner });
91206
91463
  console.log(`[ClawRouter] Solana wallet: ${solanaAddress}`);
91207
91464
  }
91208
- x402.onAfterPaymentCreation(async (context) => {
91465
+ x402?.onAfterPaymentCreation(async (context) => {
91209
91466
  if (!context.selectedRequirements.network.startsWith("eip155")) return;
91210
91467
  const payload = context.paymentPayload;
91211
91468
  payload.extensions = withBuilderCodeServiceCode(payload.extensions);
91212
91469
  });
91213
- x402.onAfterPaymentCreation(async (context) => {
91470
+ x402?.onAfterPaymentCreation(async (context) => {
91214
91471
  const network = context.selectedRequirements.network;
91215
91472
  const chain3 = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
91216
91473
  const amountMicros = parseInt(context.selectedRequirements.amount || "0", 10);
@@ -91219,16 +91476,22 @@ async function startProxy(options) {
91219
91476
  if (store) store.amountUsd = amountUsd;
91220
91477
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
91221
91478
  });
91222
- const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
91479
+ const payFetch = authMode === "api-key" ? createApiKeyFetch(apiKey) : createPayFetchWithPreAuth(fetch, x402, void 0, {
91223
91480
  skipPreAuth: paymentChain === "solana",
91224
91481
  // Per-request cost estimate so pre-auth is only reused when the cached
91225
91482
  // payment still covers the (possibly larger) request — BlockRun prices per
91226
91483
  // token, so one model can cost different amounts across requests.
91227
- 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
91228
91489
  });
91229
91490
  let balanceMonitor;
91230
91491
  if (options._balanceMonitorOverride) {
91231
91492
  balanceMonitor = options._balanceMonitorOverride;
91493
+ } else if (authMode === "api-key") {
91494
+ balanceMonitor = new ApiKeyBalanceMonitor();
91232
91495
  } else if (paymentChain === "solana" && solanaAddress) {
91233
91496
  const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
91234
91497
  balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
@@ -91270,11 +91533,17 @@ async function startProxy(options) {
91270
91533
  const full = url2.searchParams.get("full") === "true";
91271
91534
  const response = {
91272
91535
  status: "ok",
91273
- wallet: account.address,
91274
- paymentChain
91536
+ authMode
91275
91537
  };
91276
- if (solanaAddress) {
91277
- response.solana = solanaAddress;
91538
+ if (authMode === "api-key") {
91539
+ response.apiKey = maskApiKey(apiKey);
91540
+ response.gateway = apiBase;
91541
+ } else {
91542
+ response.wallet = account.address;
91543
+ response.paymentChain = paymentChain;
91544
+ if (solanaAddress) {
91545
+ response.solana = solanaAddress;
91546
+ }
91278
91547
  }
91279
91548
  if (upstreamProxy) {
91280
91549
  response.upstreamProxy = upstreamProxy;
@@ -91290,9 +91559,15 @@ async function startProxy(options) {
91290
91559
  ).unref()
91291
91560
  )
91292
91561
  ]);
91293
- response.balance = balanceInfo.balanceUSD;
91294
- response.isLow = balanceInfo.isLow;
91295
- response.isEmpty = balanceInfo.isEmpty;
91562
+ if (authMode === "api-key") {
91563
+ response.balance = null;
91564
+ response.billing = "BlockRun account credit";
91565
+ response.topUpUrl = PORTAL_CREDITS_URL;
91566
+ } else {
91567
+ response.balance = balanceInfo.balanceUSD;
91568
+ response.isLow = balanceInfo.isLow;
91569
+ response.isEmpty = balanceInfo.isEmpty;
91570
+ }
91296
91571
  } catch {
91297
91572
  response.balanceError = "Could not fetch balance";
91298
91573
  }
@@ -91472,7 +91747,7 @@ async function startProxy(options) {
91472
91747
  res.end("Bad request");
91473
91748
  return;
91474
91749
  }
91475
- const filePath = join10(IMAGE_DIR, filename);
91750
+ const filePath = join11(IMAGE_DIR, filename);
91476
91751
  try {
91477
91752
  const s3 = await fsStat(filePath);
91478
91753
  if (!s3.isFile()) throw new Error("not a file");
@@ -91503,7 +91778,7 @@ async function startProxy(options) {
91503
91778
  res.end("Bad request");
91504
91779
  return;
91505
91780
  }
91506
- const filePath = join10(AUDIO_DIR, filename);
91781
+ const filePath = join11(AUDIO_DIR, filename);
91507
91782
  try {
91508
91783
  const s3 = await fsStat(filePath);
91509
91784
  if (!s3.isFile()) throw new Error("not a file");
@@ -91533,7 +91808,7 @@ async function startProxy(options) {
91533
91808
  res.end("Bad request");
91534
91809
  return;
91535
91810
  }
91536
- const filePath = join10(VIDEO_DIR, filename);
91811
+ const filePath = join11(VIDEO_DIR, filename);
91537
91812
  try {
91538
91813
  const s3 = await fsStat(filePath);
91539
91814
  if (!s3.isFile()) throw new Error("not a file");
@@ -91666,7 +91941,7 @@ async function startProxy(options) {
91666
91941
  }
91667
91942
  }
91668
91943
  if (result.data?.length) {
91669
- await mkdir4(IMAGE_DIR, { recursive: true });
91944
+ await mkdir5(IMAGE_DIR, { recursive: true });
91670
91945
  const port2 = server.address()?.port ?? 8402;
91671
91946
  for (const img of result.data) {
91672
91947
  const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
@@ -91674,7 +91949,7 @@ async function startProxy(options) {
91674
91949
  const [, mimeType, b64] = dataUriMatch;
91675
91950
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
91676
91951
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
91677
- await writeFile2(join10(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
91952
+ await writeFile3(join11(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
91678
91953
  img.url = `http://localhost:${port2}/images/${filename}`;
91679
91954
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
91680
91955
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -91685,7 +91960,7 @@ async function startProxy(options) {
91685
91960
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
91686
91961
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
91687
91962
  const buf = Buffer.from(await imgResp.arrayBuffer());
91688
- await writeFile2(join10(IMAGE_DIR, filename), buf);
91963
+ await writeFile3(join11(IMAGE_DIR, filename), buf);
91689
91964
  img.url = `http://localhost:${port2}/images/${filename}`;
91690
91965
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
91691
91966
  }
@@ -91789,7 +92064,7 @@ async function startProxy(options) {
91789
92064
  return;
91790
92065
  }
91791
92066
  if (result.data?.length) {
91792
- await mkdir4(IMAGE_DIR, { recursive: true });
92067
+ await mkdir5(IMAGE_DIR, { recursive: true });
91793
92068
  const port2 = server.address()?.port ?? 8402;
91794
92069
  for (const img of result.data) {
91795
92070
  const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
@@ -91797,7 +92072,7 @@ async function startProxy(options) {
91797
92072
  const [, mimeType, b64] = dataUriMatch;
91798
92073
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
91799
92074
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
91800
- await writeFile2(join10(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
92075
+ await writeFile3(join11(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
91801
92076
  img.url = `http://localhost:${port2}/images/${filename}`;
91802
92077
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
91803
92078
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -91808,7 +92083,7 @@ async function startProxy(options) {
91808
92083
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
91809
92084
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
91810
92085
  const buf = Buffer.from(await imgResp.arrayBuffer());
91811
- await writeFile2(join10(IMAGE_DIR, filename), buf);
92086
+ await writeFile3(join11(IMAGE_DIR, filename), buf);
91812
92087
  img.url = `http://localhost:${port2}/images/${filename}`;
91813
92088
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
91814
92089
  }
@@ -91883,7 +92158,7 @@ async function startProxy(options) {
91883
92158
  return;
91884
92159
  }
91885
92160
  if (result.data?.length) {
91886
- await mkdir4(AUDIO_DIR, { recursive: true });
92161
+ await mkdir5(AUDIO_DIR, { recursive: true });
91887
92162
  const port2 = server.address()?.port ?? 8402;
91888
92163
  for (const track of result.data) {
91889
92164
  if (track.url?.startsWith("https://") || track.url?.startsWith("http://")) {
@@ -91894,7 +92169,7 @@ async function startProxy(options) {
91894
92169
  const ext = contentType.includes("wav") ? "wav" : "mp3";
91895
92170
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
91896
92171
  const buf = Buffer.from(await audioResp.arrayBuffer());
91897
- await writeFile2(join10(AUDIO_DIR, filename), buf);
92172
+ await writeFile3(join11(AUDIO_DIR, filename), buf);
91898
92173
  track.url = `http://localhost:${port2}/audio/${filename}`;
91899
92174
  console.log(`[ClawRouter] Audio saved \u2192 ${track.url}`);
91900
92175
  }
@@ -92043,7 +92318,7 @@ async function startProxy(options) {
92043
92318
  }
92044
92319
  }
92045
92320
  if (finalResult.data?.length) {
92046
- await mkdir4(VIDEO_DIR, { recursive: true });
92321
+ await mkdir5(VIDEO_DIR, { recursive: true });
92047
92322
  const port2 = server.address()?.port ?? 8402;
92048
92323
  for (const clip of finalResult.data) {
92049
92324
  if (clip.url?.startsWith("https://") || clip.url?.startsWith("http://")) {
@@ -92054,7 +92329,7 @@ async function startProxy(options) {
92054
92329
  const ext = contentType.includes("webm") ? "webm" : contentType.includes("quicktime") ? "mov" : "mp4";
92055
92330
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
92056
92331
  const buf = Buffer.from(await videoResp.arrayBuffer());
92057
- await writeFile2(join10(VIDEO_DIR, filename), buf);
92332
+ await writeFile3(join11(VIDEO_DIR, filename), buf);
92058
92333
  clip.url = `http://localhost:${port2}/videos/${filename}`;
92059
92334
  console.log(`[ClawRouter] Video saved \u2192 ${clip.url}`);
92060
92335
  }
@@ -92237,6 +92512,8 @@ async function startProxy(options) {
92237
92512
  port: listenPort,
92238
92513
  baseUrl: baseUrl2,
92239
92514
  walletAddress: error.wallet,
92515
+ authMode,
92516
+ ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
92240
92517
  balanceMonitor,
92241
92518
  close: async () => {
92242
92519
  }
@@ -92287,8 +92564,10 @@ async function startProxy(options) {
92287
92564
  return {
92288
92565
  port,
92289
92566
  baseUrl,
92290
- walletAddress: account.address,
92567
+ walletAddress: account?.address ?? "",
92291
92568
  solanaAddress,
92569
+ authMode,
92570
+ ...apiKey ? { apiKeyLabel: maskApiKey(apiKey) } : {},
92292
92571
  balanceMonitor,
92293
92572
  close: () => new Promise((res, rej) => {
92294
92573
  const timeout = setTimeout(() => {
@@ -94611,6 +94890,7 @@ var init_proxy = __esm({
94611
94890
  init_response_cache();
94612
94891
  init_balance();
94613
94892
  init_auth();
94893
+ init_api_key();
94614
94894
  init_spend_control();
94615
94895
  init_compression();
94616
94896
  init_version4();
@@ -94627,10 +94907,10 @@ var init_proxy = __esm({
94627
94907
  paymentStore = new AsyncLocalStorage();
94628
94908
  BLOCKRUN_API = "https://blockrun.ai/api";
94629
94909
  BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
94630
- DESKTOP_SERVICE_TOKEN_FILE = process.env.CLAWROUTER_DESKTOP_TOKEN_FILE ?? join10(homedir7(), ".clawrouter-desktop", "service-token");
94631
- IMAGE_DIR = join10(homedir7(), ".openclaw", "blockrun", "images");
94632
- AUDIO_DIR = join10(homedir7(), ".openclaw", "blockrun", "audio");
94633
- VIDEO_DIR = join10(homedir7(), ".openclaw", "blockrun", "videos");
94910
+ DESKTOP_SERVICE_TOKEN_FILE = process.env.CLAWROUTER_DESKTOP_TOKEN_FILE ?? join11(homedir8(), ".clawrouter-desktop", "service-token");
94911
+ IMAGE_DIR = join11(homedir8(), ".openclaw", "blockrun", "images");
94912
+ AUDIO_DIR = join11(homedir8(), ".openclaw", "blockrun", "audio");
94913
+ VIDEO_DIR = join11(homedir8(), ".openclaw", "blockrun", "videos");
94634
94914
  AUTO_MODEL = "blockrun/auto";
94635
94915
  ROUTING_PROFILES = /* @__PURE__ */ new Set([
94636
94916
  "blockrun/eco",
@@ -116822,8 +117102,8 @@ var init_constants3 = __esm({
116822
117102
 
116823
117103
  // src/polymarket/wallet-adapter.ts
116824
117104
  import fs2 from "fs";
116825
- import { homedir as homedir8 } from "os";
116826
- import { join as join11 } from "path";
117105
+ import { homedir as homedir9 } from "os";
117106
+ import { join as join12 } from "path";
116827
117107
  function getOrCreateWalletKey() {
116828
117108
  const envKey = process.env.BLOCKRUN_WALLET_KEY?.trim();
116829
117109
  if (envKey && /^0x[0-9a-fA-F]{64}$/.test(envKey)) return envKey;
@@ -116871,7 +117151,7 @@ var init_wallet_adapter = __esm({
116871
117151
  init_esm();
116872
117152
  init_chains();
116873
117153
  init_constants3();
116874
- WALLET_FILE2 = join11(homedir8(), ".openclaw", "blockrun", "wallet.key");
117154
+ WALLET_FILE2 = join12(homedir9(), ".openclaw", "blockrun", "wallet.key");
116875
117155
  BASE_RPC_URLS = [
116876
117156
  "https://mainnet.base.org",
116877
117157
  "https://base.llamarpc.com",
@@ -117273,9 +117553,7 @@ var init_client5 = __esm({
117273
117553
 
117274
117554
  // src/polymarket/spend-policy.ts
117275
117555
  function resolveSpendControl(deps) {
117276
- if (deps?.spendControl) return deps.spendControl;
117277
- defaultControl ??= new SpendControl();
117278
- return defaultControl;
117556
+ return deps?.spendControl ?? getSharedSpendControl();
117279
117557
  }
117280
117558
  function usdToMicros(usd) {
117281
117559
  return String(Math.ceil(usd * 1e6));
@@ -117292,7 +117570,6 @@ async function signUnderSpendPolicy(deps, quote, action, sign4) {
117292
117570
  throw err;
117293
117571
  }
117294
117572
  }
117295
- var defaultControl;
117296
117573
  var init_spend_policy = __esm({
117297
117574
  "src/polymarket/spend-policy.ts"() {
117298
117575
  "use strict";
@@ -226657,7 +226934,7 @@ var init_fund = __esm({
226657
226934
  });
226658
226935
 
226659
226936
  // src/polymarket/tool.ts
226660
- function buildPolymarketTool() {
226937
+ function buildPolymarketTool(deps) {
226661
226938
  return {
226662
226939
  name: "blockrun_polymarket",
226663
226940
  description: DESCRIPTION,
@@ -226742,26 +227019,32 @@ function buildPolymarketTool() {
226742
227019
  result = await runSetup({ confirm });
226743
227020
  break;
226744
227021
  case "fund":
226745
- result = await fundVault({
226746
- amount_usd: params.amount_usd,
226747
- confirm
226748
- });
227022
+ result = await fundVault(
227023
+ {
227024
+ amount_usd: params.amount_usd,
227025
+ confirm
227026
+ },
227027
+ deps
227028
+ );
226749
227029
  break;
226750
227030
  case "buy":
226751
227031
  case "sell":
226752
- result = await executeTrade({
226753
- action,
226754
- token_id: params.token_id,
226755
- condition_id: params.condition_id,
226756
- outcome: params.outcome,
226757
- price: params.price,
226758
- size: params.size,
226759
- amount_usd: params.amount_usd,
226760
- order_type: params.order_type,
226761
- expires_at: params.expires_at,
226762
- post_only: params.post_only,
226763
- confirm
226764
- });
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
+ );
226765
227048
  break;
226766
227049
  case "orders":
226767
227050
  result = await listOpenOrders({
@@ -226778,17 +227061,23 @@ function buildPolymarketTool() {
226778
227061
  result = await listPositions();
226779
227062
  break;
226780
227063
  case "redeem":
226781
- result = await redeemPosition({
226782
- condition_id: params.condition_id,
226783
- confirm
226784
- });
227064
+ result = await redeemPosition(
227065
+ {
227066
+ condition_id: params.condition_id,
227067
+ confirm
227068
+ },
227069
+ deps
227070
+ );
226785
227071
  break;
226786
227072
  case "withdraw":
226787
- result = await withdrawFunds({
226788
- amount_usd: params.amount_usd,
226789
- to_address: params.to_address,
226790
- confirm
226791
- });
227073
+ result = await withdrawFunds(
227074
+ {
227075
+ amount_usd: params.amount_usd,
227076
+ to_address: params.to_address,
227077
+ confirm
227078
+ },
227079
+ deps
227080
+ );
226792
227081
  break;
226793
227082
  default:
226794
227083
  throw new Error(`Unknown blockrun_polymarket action: ${action}`);
@@ -227387,9 +227676,9 @@ import {
227387
227676
  unlinkSync
227388
227677
  } from "fs";
227389
227678
  import { readFile as readFileAsync } from "fs/promises";
227390
- import { homedir as homedir13 } from "os";
227679
+ import { homedir as homedir14 } from "os";
227391
227680
  import { randomUUID } from "crypto";
227392
- import { join as join16 } from "path";
227681
+ import { join as join17 } from "path";
227393
227682
  async function waitForProxyHealth(port, timeoutMs = 3e3) {
227394
227683
  const start = Date.now();
227395
227684
  while (Date.now() - start < timeoutMs) {
@@ -227431,8 +227720,8 @@ function isBlockrunWebSearchDisabled(config) {
227431
227720
  return cfg.tools?.web?.search?.enabled === false;
227432
227721
  }
227433
227722
  function injectModelsConfig(logger48, options = {}) {
227434
- const configDir = join16(homedir13(), ".openclaw");
227435
- const configPath = join16(configDir, "openclaw.json");
227723
+ const configDir = join17(homedir14(), ".openclaw");
227724
+ const configPath = join17(configDir, "openclaw.json");
227436
227725
  let config = {};
227437
227726
  let needsWrite = false;
227438
227727
  if (!existsSync3(configDir)) {
@@ -227653,7 +227942,7 @@ function injectModelsConfig(logger48, options = {}) {
227653
227942
  }
227654
227943
  function syncAgentModelCache(logger48, options = {}) {
227655
227944
  if (!isGatewayMode() && !options.forceWrite) return;
227656
- const agentsDir = join16(homedir13(), ".openclaw", "agents");
227945
+ const agentsDir = join17(homedir14(), ".openclaw", "agents");
227657
227946
  if (!existsSync3(agentsDir)) return;
227658
227947
  let agentDirs;
227659
227948
  try {
@@ -227663,7 +227952,7 @@ function syncAgentModelCache(logger48, options = {}) {
227663
227952
  }
227664
227953
  const expectedIds = VISIBLE_OPENCLAW_MODELS.map((m) => m.id);
227665
227954
  for (const agent of agentDirs) {
227666
- const cachePath = join16(agentsDir, agent, "agent", "models.json");
227955
+ const cachePath = join17(agentsDir, agent, "agent", "models.json");
227667
227956
  if (!existsSync3(cachePath)) continue;
227668
227957
  try {
227669
227958
  const raw = readTextFileSync(cachePath).trim();
@@ -227692,7 +227981,7 @@ function syncAgentModelCache(logger48, options = {}) {
227692
227981
  }
227693
227982
  }
227694
227983
  function injectAuthProfile(logger48) {
227695
- const agentsDir = join16(homedir13(), ".openclaw", "agents");
227984
+ const agentsDir = join17(homedir14(), ".openclaw", "agents");
227696
227985
  if (!existsSync3(agentsDir)) {
227697
227986
  try {
227698
227987
  mkdirSync3(agentsDir, { recursive: true });
@@ -227709,9 +227998,9 @@ function injectAuthProfile(logger48) {
227709
227998
  agents = ["main", ...agents];
227710
227999
  }
227711
228000
  for (const agentId of agents) {
227712
- const authDir = join16(agentsDir, agentId, "agent");
227713
- const authPath = join16(authDir, "auth-profiles.json");
227714
- const sqlitePath = join16(authDir, "openclaw-agent.sqlite");
228001
+ const authDir = join17(agentsDir, agentId, "agent");
228002
+ const authPath = join17(authDir, "auth-profiles.json");
228003
+ const sqlitePath = join17(authDir, "openclaw-agent.sqlite");
227715
228004
  if (existsSync3(sqlitePath)) {
227716
228005
  removeInjectedAuthPlaceholder(authPath, logger48, agentId);
227717
228006
  continue;
@@ -227858,9 +228147,21 @@ async function startProxyInBackground(api, startupGeneration) {
227858
228147
  if (startupGeneration !== void 0 && isProxyStartupCurrent(startupGeneration, proc)) {
227859
228148
  proc.__clawrouterStartupPhase = "starting";
227860
228149
  }
228150
+ const pluginApiKey = api.pluginConfig?.apiKey;
228151
+ const resolvedApiKey = typeof pluginApiKey === "string" && isValidApiKey(pluginApiKey) ? { key: pluginApiKey.trim(), source: "config" } : await resolveApiKey();
228152
+ if (typeof pluginApiKey === "string" && !isValidApiKey(pluginApiKey)) {
228153
+ api.logger.warn(
228154
+ `pluginConfig.apiKey is set but invalid (expected brk_...) \u2014 ignoring it and looking elsewhere`
228155
+ );
228156
+ }
228157
+ const apiKey = resolvedApiKey?.key;
227861
228158
  const configKey = api.pluginConfig?.walletKey;
227862
228159
  let wallet;
227863
- if (typeof configKey === "string" && /^0x[0-9a-fA-F]{64}$/.test(configKey)) {
228160
+ if (apiKey) {
228161
+ api.logger.info(
228162
+ `Using BlockRun API key ${maskApiKey(apiKey)} (from ${resolvedApiKey.source}) \u2014 billing account credit, no wallet`
228163
+ );
228164
+ } else if (typeof configKey === "string" && /^0x[0-9a-fA-F]{64}$/.test(configKey)) {
227864
228165
  const account = privateKeyToAccount(configKey);
227865
228166
  wallet = { key: configKey, address: account.address, source: "config" };
227866
228167
  } else {
@@ -227871,7 +228172,8 @@ async function startProxyInBackground(api, startupGeneration) {
227871
228172
  }
227872
228173
  wallet = await resolveOrGenerateWalletKey();
227873
228174
  }
227874
- if (wallet.source === "generated") {
228175
+ if (!wallet) {
228176
+ } else if (wallet.source === "generated") {
227875
228177
  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`);
227876
228178
  api.logger.warn(` NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW!`);
227877
228179
  api.logger.warn(` Address : ${wallet.address}`);
@@ -227895,14 +228197,20 @@ async function startProxyInBackground(api, startupGeneration) {
227895
228197
  `Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
227896
228198
  );
227897
228199
  }
228200
+ const sharedControl = getSharedSpendControl();
228201
+ sharedControl.reloadLimits();
228202
+ sharedControl.resetSession();
227898
228203
  const proxy = await startProxy({
227899
- wallet,
228204
+ ...apiKey ? { apiKey } : { wallet },
227900
228205
  routingConfig,
227901
228206
  maxCostPerRunUsd,
227902
228207
  maxCostPerRunMode,
227903
- 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,
227904
228212
  onReady: (port) => {
227905
- api.logger.info(`BlockRun x402 proxy listening on port ${port}`);
228213
+ api.logger.info(`BlockRun ${apiKey ? "API-key" : "x402"} proxy listening on port ${port}`);
227906
228214
  },
227907
228215
  onError: (error) => {
227908
228216
  api.logger.error(`BlockRun proxy error: ${error.message}`);
@@ -227948,6 +228256,10 @@ async function startProxyInBackground(api, startupGeneration) {
227948
228256
  }
227949
228257
  api.logger.info(`ClawRouter ready \u2014 smart routing enabled`);
227950
228258
  api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);
228259
+ if (apiKey) {
228260
+ api.logger.info(`Billing: BlockRun account credit \u2014 top up at ${PORTAL_CREDITS_URL}`);
228261
+ return true;
228262
+ }
227951
228263
  const currentChain = await resolvePaymentChain();
227952
228264
  const displayAddress = currentChain === "solana" && proxy.solanaAddress ? proxy.solanaAddress : wallet.address;
227953
228265
  const network = currentChain === "solana" ? "Solana" : "Base";
@@ -228191,7 +228503,7 @@ function buildImageGenerationProvider() {
228191
228503
  (result.data ?? []).map(async (img) => {
228192
228504
  const filename = img.url?.split("/images/").pop();
228193
228505
  if (!filename) throw new Error(`Unexpected image URL format: ${img.url}`);
228194
- const filePath = join16(IMAGE_DIR2, filename);
228506
+ const filePath = join17(IMAGE_DIR2, filename);
228195
228507
  const buffer2 = await readFileAsync(filePath);
228196
228508
  const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
228197
228509
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
@@ -228243,7 +228555,7 @@ function buildMusicGenerationProvider() {
228243
228555
  (result.data ?? []).map(async (track) => {
228244
228556
  const filename = track.url?.split("/audio/").pop();
228245
228557
  if (!filename) throw new Error(`Unexpected audio URL format: ${track.url}`);
228246
- const filePath = join16(AUDIO_DIR2, filename);
228558
+ const filePath = join17(AUDIO_DIR2, filename);
228247
228559
  const buffer2 = await readFileAsync(filePath);
228248
228560
  const ext = filename.split(".").pop()?.toLowerCase() ?? "mp3";
228249
228561
  const mimeType = ext === "wav" ? "audio/wav" : "audio/mpeg";
@@ -228319,7 +228631,7 @@ function buildVideoGenerationProvider() {
228319
228631
  (result.data ?? []).map(async (clip) => {
228320
228632
  const filename = clip.url?.split("/videos/").pop();
228321
228633
  if (!filename) throw new Error(`Unexpected video URL format: ${clip.url}`);
228322
- const filePath = join16(VIDEO_DIR2, filename);
228634
+ const filePath = join17(VIDEO_DIR2, filename);
228323
228635
  const buffer2 = await readFileAsync(filePath);
228324
228636
  const ext = filename.split(".").pop()?.toLowerCase() ?? "mp4";
228325
228637
  const mimeType = ext === "webm" ? "video/webm" : ext === "mov" ? "video/quicktime" : "video/mp4";
@@ -228372,6 +228684,22 @@ function createWalletCommand(api) {
228372
228684
  requireAuth: true,
228373
228685
  handler: async (ctx) => {
228374
228686
  const subcommand = ctx.args?.trim().toLowerCase() || "status";
228687
+ const activeApiKey = (await resolveApiKey())?.key;
228688
+ if (activeApiKey) {
228689
+ return {
228690
+ text: [
228691
+ "**BlockRun account** (API key)",
228692
+ "",
228693
+ `**Key:** \`${maskApiKey(activeApiKey)}\``,
228694
+ "**Billing:** account credit \u2014 no wallet, no payment chain, no gas.",
228695
+ "",
228696
+ `**Add credit:** ${PORTAL_CREDITS_URL}`,
228697
+ `**Usage & keys:** ${PORTAL_KEYS_URL}`,
228698
+ "",
228699
+ "To pay with a USDC wallet over x402 instead, run `clawrouter logout` and restart."
228700
+ ].join("\n")
228701
+ };
228702
+ }
228375
228703
  let wallet;
228376
228704
  try {
228377
228705
  wallet = subcommand === "status" || subcommand === "export" ? await resolveExistingWalletKey() : await resolveOrGenerateWalletKey();
@@ -228623,11 +228951,12 @@ function createWalletCommand(api) {
228623
228951
  }
228624
228952
  };
228625
228953
  }
228626
- 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;
228627
228955
  var init_index = __esm({
228628
228956
  "src/index.ts"() {
228629
228957
  init_provider();
228630
228958
  init_proxy();
228959
+ init_api_key();
228631
228960
  init_web_search_provider();
228632
228961
  init_auth();
228633
228962
  init_balance();
@@ -228640,10 +228969,10 @@ var init_index = __esm({
228640
228969
  init_stats();
228641
228970
  init_partners();
228642
228971
  init_tool();
228972
+ init_spend_control();
228643
228973
  init_stats2();
228644
228974
  init_exclude();
228645
228975
  init_policy();
228646
- init_spend_control();
228647
228976
  init_mcp_config();
228648
228977
  init_openclaw_plugin_config();
228649
228978
  init_openclaw_plugin_config();
@@ -228665,11 +228994,10 @@ var init_index = __esm({
228665
228994
  init_response_cache();
228666
228995
  init_partners();
228667
228996
  activeProxyHandle = null;
228668
- liveSpendControl = null;
228669
228997
  pendingConfiguredStartupApi = null;
228670
- IMAGE_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "images");
228671
- AUDIO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "audio");
228672
- VIDEO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "videos");
228998
+ IMAGE_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "images");
228999
+ AUDIO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "audio");
229000
+ VIDEO_DIR2 = join17(homedir14(), ".openclaw", "blockrun", "videos");
228673
229001
  plugin = {
228674
229002
  // NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
228675
229003
  // 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
@@ -228981,7 +229309,7 @@ ${errText}`
228981
229309
  api.registerCommand(createExcludeCommand());
228982
229310
  api.registerCommand(
228983
229311
  createPolicyCommand({
228984
- liveControl: () => activeProxyHandle ? liveSpendControl ?? void 0 : void 0
229312
+ liveControl: () => process.__clawrouterProxyStarted ? getSharedSpendControl() : void 0
228985
229313
  })
228986
229314
  );
228987
229315
  if (shouldLogRegistration) {
@@ -229086,7 +229414,7 @@ ${errText}`
229086
229414
  }
229087
229415
  resetProxyStartupState();
229088
229416
  try {
229089
- const configPath = join16(homedir13(), ".openclaw", "openclaw.json");
229417
+ const configPath = join17(homedir14(), ".openclaw", "openclaw.json");
229090
229418
  if (existsSync3(configPath)) {
229091
229419
  const config = JSON.parse(readTextFileSync(configPath));
229092
229420
  if (config.models?.providers?.blockrun) {
@@ -229128,11 +229456,11 @@ ${errText}`
229128
229456
  api.logger.warn(`Config cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
229129
229457
  }
229130
229458
  try {
229131
- const agentsDir = join16(homedir13(), ".openclaw", "agents");
229459
+ const agentsDir = join17(homedir14(), ".openclaw", "agents");
229132
229460
  if (existsSync3(agentsDir)) {
229133
229461
  for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
229134
229462
  if (!entry.isDirectory()) continue;
229135
- const authPath = join16(agentsDir, entry.name, "agent", "auth-profiles.json");
229463
+ const authPath = join17(agentsDir, entry.name, "agent", "auth-profiles.json");
229136
229464
  if (!existsSync3(authPath)) continue;
229137
229465
  try {
229138
229466
  const store = JSON.parse(readTextFileSync(authPath));
@@ -229179,6 +229507,7 @@ export {
229179
229507
  SolanaBalanceMonitor,
229180
229508
  SpendControl,
229181
229509
  SpendPolicyError,
229510
+ UnreadableSpendPolicyError,
229182
229511
  VISIBLE_OPENCLAW_MODELS,
229183
229512
  blockrunProvider,
229184
229513
  buildImageGenerationProvider,