@blockrun/clawrouter 0.12.262 → 0.12.264

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -32632,7 +32632,7 @@ var init_spend_control = __esm({
32632
32632
  }
32633
32633
  }
32634
32634
  if (this.limits.hourly !== void 0) {
32635
- const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2);
32635
+ const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
32636
32636
  const remaining = this.limits.hourly - hourlySpent;
32637
32637
  if (estimatedCost2 > remaining) {
32638
32638
  const oldestInWindow = this.history.find((r2) => r2.timestamp >= now2 - HOUR_MS);
@@ -32647,7 +32647,7 @@ var init_spend_control = __esm({
32647
32647
  }
32648
32648
  }
32649
32649
  if (this.limits.daily !== void 0) {
32650
- const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2);
32650
+ const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
32651
32651
  const remaining = this.limits.daily - dailySpent;
32652
32652
  if (estimatedCost2 > remaining) {
32653
32653
  const oldestInWindow = this.history.find((r2) => r2.timestamp >= now2 - DAY_MS);
@@ -32744,17 +32744,30 @@ var init_spend_control = __esm({
32744
32744
  }
32745
32745
  }
32746
32746
  }
32747
- getSpendingInWindow(from15, to) {
32747
+ // `now` is the single clock reading the caller already took to build the
32748
+ // window; it must be passed in, not re-read here. In-flight reservations are
32749
+ // "now" holds, so they count only against a window that reaches the present
32750
+ // (`to >= now`). The bug this guards against: reading the clock a SECOND time
32751
+ // inside this method (the old `to >= this.now()`) could land a millisecond
32752
+ // after the caller's `now`, flip the guard false, and silently drop the
32753
+ // pending total from the hourly/daily check — letting two concurrent payments
32754
+ // both clear the same remaining budget. Threading the caller's `now` keeps the
32755
+ // guard meaningful (a genuinely historical window with `to < now` still
32756
+ // excludes live holds) without a second, racing read. Every existing test
32757
+ // injects a frozen clock (`now: () => clock`), so the two reads always matched
32758
+ // and the sub-ms window went uncaught; see the live-clock test in
32759
+ // spend-control.test.ts.
32760
+ getSpendingInWindow(from15, to, now2) {
32748
32761
  const recorded = this.history.filter((r2) => r2.timestamp >= from15 && r2.timestamp <= to).reduce((sum, r2) => sum + r2.amount, 0);
32749
- return recorded + (to >= this.now() ? this.pendingTotal() : 0);
32762
+ return recorded + (to >= now2 ? this.pendingTotal() : 0);
32750
32763
  }
32751
32764
  getSpending(window2) {
32752
32765
  const now2 = this.now();
32753
32766
  switch (window2) {
32754
32767
  case "hourly":
32755
- return this.getSpendingInWindow(now2 - HOUR_MS, now2);
32768
+ return this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
32756
32769
  case "daily":
32757
- return this.getSpendingInWindow(now2 - DAY_MS, now2);
32770
+ return this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
32758
32771
  case "session":
32759
32772
  return this.sessionSpent + this.pendingTotal();
32760
32773
  }
@@ -32766,8 +32779,8 @@ var init_spend_control = __esm({
32766
32779
  }
32767
32780
  getStatus() {
32768
32781
  const now2 = this.now();
32769
- const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2);
32770
- const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2);
32782
+ const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
32783
+ const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
32771
32784
  return {
32772
32785
  limits: cloneLimits(this.limits),
32773
32786
  spending: {
@@ -39920,6 +39933,13 @@ var init_version4 = __esm({
39920
39933
  import { readdir, unlink } from "fs/promises";
39921
39934
  import { join as join5 } from "path";
39922
39935
  import { homedir as homedir3 } from "os";
39936
+ function resolveStatsDays(raw) {
39937
+ const parsed = parseInt(raw ?? "", 10);
39938
+ if (!Number.isFinite(parsed) || parsed <= 0) {
39939
+ return DEFAULT_STATS_DAYS;
39940
+ }
39941
+ return Math.min(parsed, MAX_STATS_DAYS);
39942
+ }
39923
39943
  async function parseLogFile(filePath) {
39924
39944
  try {
39925
39945
  const content = await readTextFile(filePath);
@@ -40105,7 +40125,8 @@ function formatStatsAscii(stats) {
40105
40125
  lines.push("\u255A\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
40106
40126
  return lines.join("\n");
40107
40127
  }
40108
- async function formatRecentLogs(days = 1) {
40128
+ async function formatRecentLogs(requestedDays = 1) {
40129
+ const days = Number.isFinite(requestedDays) && requestedDays > 0 ? Math.floor(requestedDays) : 1;
40109
40130
  const logFiles = await getLogFiles();
40110
40131
  const filesToRead = logFiles.slice(0, days);
40111
40132
  const allEntries = [];
@@ -40161,13 +40182,15 @@ async function clearStats() {
40161
40182
  return { deletedFiles: 0 };
40162
40183
  }
40163
40184
  }
40164
- var LOG_DIR2;
40185
+ var LOG_DIR2, DEFAULT_STATS_DAYS, MAX_STATS_DAYS;
40165
40186
  var init_stats = __esm({
40166
40187
  "src/stats.ts"() {
40167
40188
  "use strict";
40168
40189
  init_fs_read();
40169
40190
  init_version4();
40170
40191
  LOG_DIR2 = join5(homedir3(), ".openclaw", "blockrun", "logs");
40192
+ DEFAULT_STATS_DAYS = 7;
40193
+ MAX_STATS_DAYS = 30;
40171
40194
  }
40172
40195
  });
40173
40196
 
@@ -91086,8 +91109,8 @@ async function startProxy(options) {
91086
91109
  if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
91087
91110
  try {
91088
91111
  const url2 = new URL(req.url, "http://localhost");
91089
- const days = parseInt(url2.searchParams.get("days") || "7", 10);
91090
- const stats = await getStats(Math.min(days, 30));
91112
+ const days = resolveStatsDays(url2.searchParams.get("days"));
91113
+ const stats = await getStats(days);
91091
91114
  res.writeHead(200, {
91092
91115
  "Content-Type": "application/json",
91093
91116
  "Cache-Control": "no-cache"
@@ -117009,6 +117032,35 @@ var init_client5 = __esm({
117009
117032
  }
117010
117033
  });
117011
117034
 
117035
+ // src/polymarket/spend-policy.ts
117036
+ function resolveSpendControl(deps) {
117037
+ if (deps?.spendControl) return deps.spendControl;
117038
+ defaultControl ??= new SpendControl();
117039
+ return defaultControl;
117040
+ }
117041
+ function usdToMicros(usd) {
117042
+ return String(Math.ceil(usd * 1e6));
117043
+ }
117044
+ async function signUnderSpendPolicy(deps, quote, action, sign4) {
117045
+ const control = resolveSpendControl(deps);
117046
+ const reservation = assertSpendPolicyAllows(control, quote);
117047
+ try {
117048
+ const signed2 = await sign4();
117049
+ if (reservation !== void 0) control.settleReservation(reservation, { action });
117050
+ return signed2;
117051
+ } catch (err) {
117052
+ if (reservation !== void 0) control.releaseReservation(reservation);
117053
+ throw err;
117054
+ }
117055
+ }
117056
+ var defaultControl;
117057
+ var init_spend_policy = __esm({
117058
+ "src/polymarket/spend-policy.ts"() {
117059
+ "use strict";
117060
+ init_spend_control();
117061
+ }
117062
+ });
117063
+
117012
117064
  // src/polymarket/orders.ts
117013
117065
  function getSessionLedger() {
117014
117066
  return {
@@ -117077,6 +117129,9 @@ function isCredsMismatchError(err) {
117077
117129
  return msg.includes("unauthorized") || msg.includes("api key not found") || msg.includes("invalid api key");
117078
117130
  }
117079
117131
  async function mapClobError(err) {
117132
+ if (err instanceof SpendPolicyError) {
117133
+ return `Refused by spend policy \u2014 nothing was signed. ${err.message} (see ~/.openclaw/blockrun/spending.json)`;
117134
+ }
117080
117135
  const e7 = err;
117081
117136
  const dataText = e7?.data ? ` \u2014 ${typeof e7.data === "string" ? e7.data : JSON.stringify(e7.data)}` : "";
117082
117137
  const message = `${e7?.message ?? String(err)}${dataText}`;
@@ -117123,7 +117178,7 @@ async function withCredsRetry(fn) {
117123
117178
  return fn(fresh);
117124
117179
  }
117125
117180
  }
117126
- async function executeTrade(input) {
117181
+ async function executeTrade(input, deps) {
117127
117182
  const side = input.action === "buy" ? Side.BUY : Side.SELL;
117128
117183
  const isLimit = input.price !== void 0;
117129
117184
  if (isLimit && input.size === void 0) {
@@ -117135,6 +117190,18 @@ async function executeTrade(input) {
117135
117190
  isError: true
117136
117191
  };
117137
117192
  }
117193
+ if (!isLimit && input.action === "buy" && input.amount_usd !== void 0 && !(Number.isFinite(input.amount_usd) && input.amount_usd > 0)) {
117194
+ return {
117195
+ text: `amount_usd must be a positive dollar amount to spend, got ${input.amount_usd}.`,
117196
+ isError: true
117197
+ };
117198
+ }
117199
+ if (isLimit && input.size !== void 0 && !(Number.isFinite(input.size) && input.size > 0)) {
117200
+ return {
117201
+ text: `size must be a positive number of shares, got ${input.size}.`,
117202
+ isError: true
117203
+ };
117204
+ }
117138
117205
  if (!isLimit && input.action === "sell" && input.size === void 0) {
117139
117206
  return { text: `Market sells need size (shares to sell).`, isError: true };
117140
117207
  }
@@ -117234,39 +117301,44 @@ Re-call with confirm:true to sign and submit.`,
117234
117301
  }
117235
117302
  const options = { tickSize, negRisk };
117236
117303
  reserveBet(notional, input.agent_id);
117237
- let response;
117304
+ const policyQuote = {
117305
+ payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
117306
+ network: `eip155:${POLYGON_CHAIN_ID}`,
117307
+ asset: PUSD_COLLATERAL,
117308
+ amount: usdToMicros(notional)
117309
+ };
117310
+ let r2;
117238
117311
  try {
117239
- response = isLimit ? await clob.createAndPostOrder(
117240
- {
117241
- tokenID: token.tokenId,
117242
- price,
117243
- size: size5,
117244
- side,
117245
- ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117246
- },
117247
- options,
117248
- orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117249
- input.post_only ?? false
117250
- ) : await clob.createAndPostMarketOrder(
117251
- {
117252
- tokenID: token.tokenId,
117253
- amount: input.action === "buy" ? input.amount_usd : size5,
117254
- side,
117255
- orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117256
- },
117257
- options,
117258
- orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117259
- );
117312
+ r2 = await signUnderSpendPolicy(deps, policyQuote, "polymarket order", async () => {
117313
+ const res = await (isLimit ? clob.createAndPostOrder(
117314
+ {
117315
+ tokenID: token.tokenId,
117316
+ price,
117317
+ size: size5,
117318
+ side,
117319
+ ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117320
+ },
117321
+ options,
117322
+ orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117323
+ input.post_only ?? false
117324
+ ) : clob.createAndPostMarketOrder(
117325
+ {
117326
+ tokenID: token.tokenId,
117327
+ amount: input.action === "buy" ? input.amount_usd : size5,
117328
+ side,
117329
+ orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117330
+ },
117331
+ options,
117332
+ orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117333
+ ));
117334
+ const placed = res?.success !== false && (res?.orderID || res?.status === "matched");
117335
+ if (!placed) throw new Error(res?.errorMsg || "order rejected");
117336
+ return res;
117337
+ });
117260
117338
  } catch (submitErr) {
117261
117339
  releaseBet(notional, input.agent_id);
117262
117340
  throw submitErr;
117263
117341
  }
117264
- const r2 = response;
117265
- const placed = r2?.success !== false && (r2?.orderID || r2?.status === "matched");
117266
- if (!placed) {
117267
- releaseBet(notional, input.agent_id);
117268
- throw new Error(r2?.errorMsg || "order rejected");
117269
- }
117270
117342
  commitBet();
117271
117343
  const filled = r2?.status === "matched" || (r2?.transactionsHashes?.length ?? 0) > 0;
117272
117344
  return {
@@ -117355,6 +117427,8 @@ var init_orders = __esm({
117355
117427
  init_client5();
117356
117428
  init_constants3();
117357
117429
  init_creds();
117430
+ init_spend_control();
117431
+ init_spend_policy();
117358
117432
  ledger = {
117359
117433
  totalUsd: 0,
117360
117434
  count: 0,
@@ -225289,7 +225363,7 @@ var init_setup = __esm({
225289
225363
  });
225290
225364
 
225291
225365
  // src/polymarket/redeem.ts
225292
- async function redeemPosition(input) {
225366
+ async function redeemPosition(input, deps) {
225293
225367
  if (!input.condition_id) {
225294
225368
  return {
225295
225369
  text: `Pass condition_id:"0x\u2026" (see action:"positions" for redeemable markets).`,
@@ -225365,20 +225439,31 @@ async function redeemPosition(input) {
225365
225439
  ]
225366
225440
  });
225367
225441
  const target = negRisk ? NEG_RISK_ADAPTER : CONDITIONAL_TOKENS;
225368
- let txHash;
225369
- if (getSigType() === 3) {
225370
- const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225371
- txHash = res.transactionHash;
225372
- } else {
225373
- const account = getPolymarketAccount();
225374
- const wallet = createWalletClient({
225375
- account,
225376
- chain: polygon,
225377
- transport: http(POLYGON_RPC_URLS[0])
225378
- });
225379
- txHash = await wallet.sendTransaction({ to: target, data, chain: polygon, account });
225380
- await pc.waitForTransactionReceipt({ hash: txHash });
225381
- }
225442
+ const eoa = getSigType() !== 3;
225443
+ const txHash = await signUnderSpendPolicy(
225444
+ deps,
225445
+ {
225446
+ payTo: target,
225447
+ network: `eip155:${POLYGON_CHAIN_ID}`,
225448
+ asset: PUSD_COLLATERAL,
225449
+ amount: "0"
225450
+ },
225451
+ "polymarket redeem",
225452
+ async () => {
225453
+ if (!eoa) {
225454
+ const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225455
+ return res.transactionHash;
225456
+ }
225457
+ const account = getPolymarketAccount();
225458
+ const wallet = createWalletClient({
225459
+ account,
225460
+ chain: polygon,
225461
+ transport: http(POLYGON_RPC_URLS[0])
225462
+ });
225463
+ return wallet.sendTransaction({ to: target, data, chain: polygon, account });
225464
+ }
225465
+ );
225466
+ if (eoa) await pc.waitForTransactionReceipt({ hash: txHash });
225382
225467
  const balanceAfter = await getPusdBalance(owner).catch(() => null);
225383
225468
  return {
225384
225469
  text: [
@@ -225409,6 +225494,7 @@ var init_redeem = __esm({
225409
225494
  init_positions();
225410
225495
  init_relayer();
225411
225496
  init_setup();
225497
+ init_spend_policy();
225412
225498
  }
225413
225499
  });
225414
225500
 
@@ -225421,7 +225507,13 @@ async function rawPusdBalance(owner) {
225421
225507
  args: [owner]
225422
225508
  });
225423
225509
  }
225424
- async function withdrawFunds(input) {
225510
+ async function withdrawFunds(input, deps) {
225511
+ if (input.amount_usd !== void 0 && !(Number.isFinite(input.amount_usd) && input.amount_usd > 0)) {
225512
+ return {
225513
+ text: `amount_usd must be a positive dollar amount, got ${input.amount_usd}. Omit it to withdraw the full balance.`,
225514
+ isError: true
225515
+ };
225516
+ }
225425
225517
  let owner;
225426
225518
  try {
225427
225519
  owner = getFundsAddress();
@@ -225492,29 +225584,40 @@ async function withdrawFunds(input) {
225492
225584
  functionName: "transfer",
225493
225585
  args: [bridgeEvm, amountRaw]
225494
225586
  });
225495
- let txHash;
225496
- if (getSigType() === 3) {
225497
- const res = await sendWalletBatch(
225498
- [{ target: PUSD_COLLATERAL, value: "0", data }],
225499
- owner,
225500
- "Withdraw"
225501
- );
225502
- txHash = res.transactionHash;
225503
- } else {
225504
- const account = getPolymarketAccount();
225505
- const wallet = createWalletClient({
225506
- account,
225507
- chain: polygon,
225508
- transport: http(POLYGON_RPC_URLS[0])
225509
- });
225510
- txHash = await wallet.sendTransaction({
225511
- to: PUSD_COLLATERAL,
225512
- data,
225513
- chain: polygon,
225514
- account
225515
- });
225516
- await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225517
- }
225587
+ const eoa = getSigType() !== 3;
225588
+ const txHash = await signUnderSpendPolicy(
225589
+ deps,
225590
+ {
225591
+ payTo: recipient,
225592
+ network: `eip155:${BASE_CHAIN_ID}`,
225593
+ asset: BASE_USDC,
225594
+ amount: amountRaw.toString()
225595
+ },
225596
+ "polymarket withdraw",
225597
+ async () => {
225598
+ if (!eoa) {
225599
+ const res = await sendWalletBatch(
225600
+ [{ target: PUSD_COLLATERAL, value: "0", data }],
225601
+ owner,
225602
+ "Withdraw"
225603
+ );
225604
+ return res.transactionHash;
225605
+ }
225606
+ const account = getPolymarketAccount();
225607
+ const wallet = createWalletClient({
225608
+ account,
225609
+ chain: polygon,
225610
+ transport: http(POLYGON_RPC_URLS[0])
225611
+ });
225612
+ return wallet.sendTransaction({
225613
+ to: PUSD_COLLATERAL,
225614
+ data,
225615
+ chain: polygon,
225616
+ account
225617
+ });
225618
+ }
225619
+ );
225620
+ if (eoa) await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225518
225621
  return {
225519
225622
  text: [
225520
225623
  `\u2705 Withdrawal submitted: $${amountUsd.toFixed(2)} pUSD \u2192 USDC on Base`,
@@ -225549,6 +225652,7 @@ var init_withdraw = __esm({
225549
225652
  init_positions();
225550
225653
  init_relayer();
225551
225654
  init_setup();
225655
+ init_spend_policy();
225552
225656
  }
225553
225657
  });
225554
225658
 
@@ -226194,7 +226298,7 @@ async function bridgeAddressFor(vault) {
226194
226298
  throw new Error(`Bridge did not return a deposit address (got: ${JSON.stringify(res.data)}).`);
226195
226299
  return evm;
226196
226300
  }
226197
- async function fundVault(input) {
226301
+ async function fundVault(input, deps) {
226198
226302
  if (input.amount_usd === void 0 || input.amount_usd <= 0) {
226199
226303
  return {
226200
226304
  text: `Pass amount_usd \u2014 the USDC amount to move from your Base wallet into your Polymarket vault (e.g. amount_usd:5).`,
@@ -226251,12 +226355,12 @@ async function fundVault(input) {
226251
226355
  }
226252
226356
  const privateKey = getOrCreateWalletKey();
226253
226357
  const amountMicro = String(Math.floor(amountUsd * 10 ** USDC_DECIMALS));
226254
- const depositAuthorization = await createPaymentPayload(
226255
- privateKey,
226256
- agent,
226257
- bridge,
226258
- amountMicro,
226259
- `eip155:${BASE_CHAIN_ID}`
226358
+ const network = `eip155:${BASE_CHAIN_ID}`;
226359
+ const depositAuthorization = await signUnderSpendPolicy(
226360
+ deps,
226361
+ { payTo: bridge, network, asset: BASE_USDC, amount: amountMicro },
226362
+ "polymarket fund",
226363
+ () => createPaymentPayload(privateKey, agent, bridge, amountMicro, network)
226260
226364
  );
226261
226365
  const client = new BlockrunClient({ privateKey });
226262
226366
  const result = await client.post("/v1/polymarket/fund", {
@@ -226306,6 +226410,7 @@ var init_fund = __esm({
226306
226410
  init_constants3();
226307
226411
  init_positions();
226308
226412
  init_setup();
226413
+ init_spend_policy();
226309
226414
  FUND_FEE_USD = 0.01;
226310
226415
  USDC_DECIMALS = 6;
226311
226416
  FUND_MIN_USD = Number(process.env.POLYMARKET_FUND_MIN_USD || "2");
@@ -228605,6 +228710,7 @@ init_balance();
228605
228710
  init_wallet2();
228606
228711
  init_stats();
228607
228712
  init_proxy();
228713
+ init_spend_control();
228608
228714
  init_version4();
228609
228715
  import { platform, arch, freemem, totalmem } from "os";
228610
228716
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
@@ -228871,6 +228977,15 @@ var DOCTOR_MODELS = {
228871
228977
  cost: "~$0.01"
228872
228978
  }
228873
228979
  };
228980
+ function createDoctorX402Client(opts) {
228981
+ const account = privateKeyToAccount(opts.walletKey);
228982
+ const publicClient = createPublicClient({ chain: base, transport: http() });
228983
+ const evmSigner = toClientEvmSigner(account, publicClient);
228984
+ const x402 = new x402Client();
228985
+ registerSpendPolicyHook(x402, opts.spendControl ?? new SpendControl());
228986
+ registerExactEvmScheme(x402, { signer: evmSigner });
228987
+ return x402;
228988
+ }
228874
228989
  async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
228875
228990
  if (diagnostics.wallet.isEmpty) {
228876
228991
  console.log("\n\u{1F4B3} Wallet is empty - cannot call AI for analysis.");
@@ -228888,11 +229003,7 @@ async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
228888
229003
  `);
228889
229004
  try {
228890
229005
  const { key: key2 } = await resolveOrGenerateWalletKey();
228891
- const account = privateKeyToAccount(key2);
228892
- const publicClient = createPublicClient({ chain: base, transport: http() });
228893
- const evmSigner = toClientEvmSigner(account, publicClient);
228894
- const x402 = new x402Client();
228895
- registerExactEvmScheme(x402, { signer: evmSigner });
229006
+ const x402 = createDoctorX402Client({ walletKey: key2 });
228896
229007
  const paymentChain = diagnostics.wallet.paymentChain;
228897
229008
  if (paymentChain === "solana") {
228898
229009
  try {
package/dist/index.d.ts CHANGED
@@ -1467,12 +1467,6 @@ declare function fetchWithRetry(fetchFn: (url: string, init?: RequestInit) => Pr
1467
1467
  */
1468
1468
  declare function isRetryable(errorOrResponse: Error | Response, config?: Partial<RetryConfig>): boolean;
1469
1469
 
1470
- /**
1471
- * Usage Statistics Aggregator
1472
- *
1473
- * Reads usage log files and aggregates statistics for terminal display.
1474
- * Supports filtering by date range and provides multiple aggregation views.
1475
- */
1476
1470
  type DailyStats = {
1477
1471
  date: string;
1478
1472
  totalRequests: number;
package/dist/index.js CHANGED
@@ -34768,7 +34768,7 @@ var init_spend_control = __esm({
34768
34768
  }
34769
34769
  }
34770
34770
  if (this.limits.hourly !== void 0) {
34771
- const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2);
34771
+ const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
34772
34772
  const remaining = this.limits.hourly - hourlySpent;
34773
34773
  if (estimatedCost2 > remaining) {
34774
34774
  const oldestInWindow = this.history.find((r2) => r2.timestamp >= now2 - HOUR_MS);
@@ -34783,7 +34783,7 @@ var init_spend_control = __esm({
34783
34783
  }
34784
34784
  }
34785
34785
  if (this.limits.daily !== void 0) {
34786
- const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2);
34786
+ const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
34787
34787
  const remaining = this.limits.daily - dailySpent;
34788
34788
  if (estimatedCost2 > remaining) {
34789
34789
  const oldestInWindow = this.history.find((r2) => r2.timestamp >= now2 - DAY_MS);
@@ -34880,17 +34880,30 @@ var init_spend_control = __esm({
34880
34880
  }
34881
34881
  }
34882
34882
  }
34883
- getSpendingInWindow(from15, to) {
34883
+ // `now` is the single clock reading the caller already took to build the
34884
+ // window; it must be passed in, not re-read here. In-flight reservations are
34885
+ // "now" holds, so they count only against a window that reaches the present
34886
+ // (`to >= now`). The bug this guards against: reading the clock a SECOND time
34887
+ // inside this method (the old `to >= this.now()`) could land a millisecond
34888
+ // after the caller's `now`, flip the guard false, and silently drop the
34889
+ // pending total from the hourly/daily check — letting two concurrent payments
34890
+ // both clear the same remaining budget. Threading the caller's `now` keeps the
34891
+ // guard meaningful (a genuinely historical window with `to < now` still
34892
+ // excludes live holds) without a second, racing read. Every existing test
34893
+ // injects a frozen clock (`now: () => clock`), so the two reads always matched
34894
+ // and the sub-ms window went uncaught; see the live-clock test in
34895
+ // spend-control.test.ts.
34896
+ getSpendingInWindow(from15, to, now2) {
34884
34897
  const recorded = this.history.filter((r2) => r2.timestamp >= from15 && r2.timestamp <= to).reduce((sum, r2) => sum + r2.amount, 0);
34885
- return recorded + (to >= this.now() ? this.pendingTotal() : 0);
34898
+ return recorded + (to >= now2 ? this.pendingTotal() : 0);
34886
34899
  }
34887
34900
  getSpending(window2) {
34888
34901
  const now2 = this.now();
34889
34902
  switch (window2) {
34890
34903
  case "hourly":
34891
- return this.getSpendingInWindow(now2 - HOUR_MS, now2);
34904
+ return this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
34892
34905
  case "daily":
34893
- return this.getSpendingInWindow(now2 - DAY_MS, now2);
34906
+ return this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
34894
34907
  case "session":
34895
34908
  return this.sessionSpent + this.pendingTotal();
34896
34909
  }
@@ -34902,8 +34915,8 @@ var init_spend_control = __esm({
34902
34915
  }
34903
34916
  getStatus() {
34904
34917
  const now2 = this.now();
34905
- const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2);
34906
- const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2);
34918
+ const hourlySpent = this.getSpendingInWindow(now2 - HOUR_MS, now2, now2);
34919
+ const dailySpent = this.getSpendingInWindow(now2 - DAY_MS, now2, now2);
34907
34920
  return {
34908
34921
  limits: cloneLimits(this.limits),
34909
34922
  spending: {
@@ -39835,6 +39848,13 @@ var init_version4 = __esm({
39835
39848
  import { readdir, unlink } from "fs/promises";
39836
39849
  import { join as join5 } from "path";
39837
39850
  import { homedir as homedir3 } from "os";
39851
+ function resolveStatsDays(raw) {
39852
+ const parsed = parseInt(raw ?? "", 10);
39853
+ if (!Number.isFinite(parsed) || parsed <= 0) {
39854
+ return DEFAULT_STATS_DAYS;
39855
+ }
39856
+ return Math.min(parsed, MAX_STATS_DAYS);
39857
+ }
39838
39858
  async function parseLogFile(filePath) {
39839
39859
  try {
39840
39860
  const content = await readTextFile(filePath);
@@ -40030,13 +40050,15 @@ async function clearStats() {
40030
40050
  return { deletedFiles: 0 };
40031
40051
  }
40032
40052
  }
40033
- var LOG_DIR2;
40053
+ var LOG_DIR2, DEFAULT_STATS_DAYS, MAX_STATS_DAYS;
40034
40054
  var init_stats = __esm({
40035
40055
  "src/stats.ts"() {
40036
40056
  "use strict";
40037
40057
  init_fs_read();
40038
40058
  init_version4();
40039
40059
  LOG_DIR2 = join5(homedir3(), ".openclaw", "blockrun", "logs");
40060
+ DEFAULT_STATS_DAYS = 7;
40061
+ MAX_STATS_DAYS = 30;
40040
40062
  }
40041
40063
  });
40042
40064
 
@@ -90906,8 +90928,8 @@ async function startProxy(options) {
90906
90928
  if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
90907
90929
  try {
90908
90930
  const url2 = new URL(req.url, "http://localhost");
90909
- const days = parseInt(url2.searchParams.get("days") || "7", 10);
90910
- const stats = await getStats(Math.min(days, 30));
90931
+ const days = resolveStatsDays(url2.searchParams.get("days"));
90932
+ const stats = await getStats(days);
90911
90933
  res.writeHead(200, {
90912
90934
  "Content-Type": "application/json",
90913
90935
  "Cache-Control": "no-cache"
@@ -116829,6 +116851,35 @@ var init_client5 = __esm({
116829
116851
  }
116830
116852
  });
116831
116853
 
116854
+ // src/polymarket/spend-policy.ts
116855
+ function resolveSpendControl(deps) {
116856
+ if (deps?.spendControl) return deps.spendControl;
116857
+ defaultControl ??= new SpendControl();
116858
+ return defaultControl;
116859
+ }
116860
+ function usdToMicros(usd) {
116861
+ return String(Math.ceil(usd * 1e6));
116862
+ }
116863
+ async function signUnderSpendPolicy(deps, quote, action, sign4) {
116864
+ const control = resolveSpendControl(deps);
116865
+ const reservation = assertSpendPolicyAllows(control, quote);
116866
+ try {
116867
+ const signed2 = await sign4();
116868
+ if (reservation !== void 0) control.settleReservation(reservation, { action });
116869
+ return signed2;
116870
+ } catch (err) {
116871
+ if (reservation !== void 0) control.releaseReservation(reservation);
116872
+ throw err;
116873
+ }
116874
+ }
116875
+ var defaultControl;
116876
+ var init_spend_policy = __esm({
116877
+ "src/polymarket/spend-policy.ts"() {
116878
+ "use strict";
116879
+ init_spend_control();
116880
+ }
116881
+ });
116882
+
116832
116883
  // src/polymarket/orders.ts
116833
116884
  function getSessionLedger() {
116834
116885
  return {
@@ -116897,6 +116948,9 @@ function isCredsMismatchError(err) {
116897
116948
  return msg.includes("unauthorized") || msg.includes("api key not found") || msg.includes("invalid api key");
116898
116949
  }
116899
116950
  async function mapClobError(err) {
116951
+ if (err instanceof SpendPolicyError) {
116952
+ return `Refused by spend policy \u2014 nothing was signed. ${err.message} (see ~/.openclaw/blockrun/spending.json)`;
116953
+ }
116900
116954
  const e7 = err;
116901
116955
  const dataText = e7?.data ? ` \u2014 ${typeof e7.data === "string" ? e7.data : JSON.stringify(e7.data)}` : "";
116902
116956
  const message = `${e7?.message ?? String(err)}${dataText}`;
@@ -116943,7 +116997,7 @@ async function withCredsRetry(fn) {
116943
116997
  return fn(fresh);
116944
116998
  }
116945
116999
  }
116946
- async function executeTrade(input) {
117000
+ async function executeTrade(input, deps) {
116947
117001
  const side = input.action === "buy" ? Side.BUY : Side.SELL;
116948
117002
  const isLimit = input.price !== void 0;
116949
117003
  if (isLimit && input.size === void 0) {
@@ -116955,6 +117009,18 @@ async function executeTrade(input) {
116955
117009
  isError: true
116956
117010
  };
116957
117011
  }
117012
+ if (!isLimit && input.action === "buy" && input.amount_usd !== void 0 && !(Number.isFinite(input.amount_usd) && input.amount_usd > 0)) {
117013
+ return {
117014
+ text: `amount_usd must be a positive dollar amount to spend, got ${input.amount_usd}.`,
117015
+ isError: true
117016
+ };
117017
+ }
117018
+ if (isLimit && input.size !== void 0 && !(Number.isFinite(input.size) && input.size > 0)) {
117019
+ return {
117020
+ text: `size must be a positive number of shares, got ${input.size}.`,
117021
+ isError: true
117022
+ };
117023
+ }
116958
117024
  if (!isLimit && input.action === "sell" && input.size === void 0) {
116959
117025
  return { text: `Market sells need size (shares to sell).`, isError: true };
116960
117026
  }
@@ -117054,39 +117120,44 @@ Re-call with confirm:true to sign and submit.`,
117054
117120
  }
117055
117121
  const options = { tickSize, negRisk };
117056
117122
  reserveBet(notional, input.agent_id);
117057
- let response;
117123
+ const policyQuote = {
117124
+ payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
117125
+ network: `eip155:${POLYGON_CHAIN_ID}`,
117126
+ asset: PUSD_COLLATERAL,
117127
+ amount: usdToMicros(notional)
117128
+ };
117129
+ let r2;
117058
117130
  try {
117059
- response = isLimit ? await clob.createAndPostOrder(
117060
- {
117061
- tokenID: token.tokenId,
117062
- price,
117063
- size: size5,
117064
- side,
117065
- ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117066
- },
117067
- options,
117068
- orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117069
- input.post_only ?? false
117070
- ) : await clob.createAndPostMarketOrder(
117071
- {
117072
- tokenID: token.tokenId,
117073
- amount: input.action === "buy" ? input.amount_usd : size5,
117074
- side,
117075
- orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117076
- },
117077
- options,
117078
- orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117079
- );
117131
+ r2 = await signUnderSpendPolicy(deps, policyQuote, "polymarket order", async () => {
117132
+ const res = await (isLimit ? clob.createAndPostOrder(
117133
+ {
117134
+ tokenID: token.tokenId,
117135
+ price,
117136
+ size: size5,
117137
+ side,
117138
+ ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117139
+ },
117140
+ options,
117141
+ orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117142
+ input.post_only ?? false
117143
+ ) : clob.createAndPostMarketOrder(
117144
+ {
117145
+ tokenID: token.tokenId,
117146
+ amount: input.action === "buy" ? input.amount_usd : size5,
117147
+ side,
117148
+ orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117149
+ },
117150
+ options,
117151
+ orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117152
+ ));
117153
+ const placed = res?.success !== false && (res?.orderID || res?.status === "matched");
117154
+ if (!placed) throw new Error(res?.errorMsg || "order rejected");
117155
+ return res;
117156
+ });
117080
117157
  } catch (submitErr) {
117081
117158
  releaseBet(notional, input.agent_id);
117082
117159
  throw submitErr;
117083
117160
  }
117084
- const r2 = response;
117085
- const placed = r2?.success !== false && (r2?.orderID || r2?.status === "matched");
117086
- if (!placed) {
117087
- releaseBet(notional, input.agent_id);
117088
- throw new Error(r2?.errorMsg || "order rejected");
117089
- }
117090
117161
  commitBet();
117091
117162
  const filled = r2?.status === "matched" || (r2?.transactionsHashes?.length ?? 0) > 0;
117092
117163
  return {
@@ -117175,6 +117246,8 @@ var init_orders = __esm({
117175
117246
  init_client5();
117176
117247
  init_constants3();
117177
117248
  init_creds();
117249
+ init_spend_control();
117250
+ init_spend_policy();
117178
117251
  ledger = {
117179
117252
  totalUsd: 0,
117180
117253
  count: 0,
@@ -225109,7 +225182,7 @@ var init_setup = __esm({
225109
225182
  });
225110
225183
 
225111
225184
  // src/polymarket/redeem.ts
225112
- async function redeemPosition(input) {
225185
+ async function redeemPosition(input, deps) {
225113
225186
  if (!input.condition_id) {
225114
225187
  return {
225115
225188
  text: `Pass condition_id:"0x\u2026" (see action:"positions" for redeemable markets).`,
@@ -225185,20 +225258,31 @@ async function redeemPosition(input) {
225185
225258
  ]
225186
225259
  });
225187
225260
  const target = negRisk ? NEG_RISK_ADAPTER : CONDITIONAL_TOKENS;
225188
- let txHash;
225189
- if (getSigType() === 3) {
225190
- const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225191
- txHash = res.transactionHash;
225192
- } else {
225193
- const account = getPolymarketAccount();
225194
- const wallet = createWalletClient({
225195
- account,
225196
- chain: polygon,
225197
- transport: http(POLYGON_RPC_URLS[0])
225198
- });
225199
- txHash = await wallet.sendTransaction({ to: target, data, chain: polygon, account });
225200
- await pc.waitForTransactionReceipt({ hash: txHash });
225201
- }
225261
+ const eoa = getSigType() !== 3;
225262
+ const txHash = await signUnderSpendPolicy(
225263
+ deps,
225264
+ {
225265
+ payTo: target,
225266
+ network: `eip155:${POLYGON_CHAIN_ID}`,
225267
+ asset: PUSD_COLLATERAL,
225268
+ amount: "0"
225269
+ },
225270
+ "polymarket redeem",
225271
+ async () => {
225272
+ if (!eoa) {
225273
+ const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225274
+ return res.transactionHash;
225275
+ }
225276
+ const account = getPolymarketAccount();
225277
+ const wallet = createWalletClient({
225278
+ account,
225279
+ chain: polygon,
225280
+ transport: http(POLYGON_RPC_URLS[0])
225281
+ });
225282
+ return wallet.sendTransaction({ to: target, data, chain: polygon, account });
225283
+ }
225284
+ );
225285
+ if (eoa) await pc.waitForTransactionReceipt({ hash: txHash });
225202
225286
  const balanceAfter = await getPusdBalance(owner).catch(() => null);
225203
225287
  return {
225204
225288
  text: [
@@ -225229,6 +225313,7 @@ var init_redeem = __esm({
225229
225313
  init_positions();
225230
225314
  init_relayer();
225231
225315
  init_setup();
225316
+ init_spend_policy();
225232
225317
  }
225233
225318
  });
225234
225319
 
@@ -225241,7 +225326,13 @@ async function rawPusdBalance(owner) {
225241
225326
  args: [owner]
225242
225327
  });
225243
225328
  }
225244
- async function withdrawFunds(input) {
225329
+ async function withdrawFunds(input, deps) {
225330
+ if (input.amount_usd !== void 0 && !(Number.isFinite(input.amount_usd) && input.amount_usd > 0)) {
225331
+ return {
225332
+ text: `amount_usd must be a positive dollar amount, got ${input.amount_usd}. Omit it to withdraw the full balance.`,
225333
+ isError: true
225334
+ };
225335
+ }
225245
225336
  let owner;
225246
225337
  try {
225247
225338
  owner = getFundsAddress();
@@ -225312,29 +225403,40 @@ async function withdrawFunds(input) {
225312
225403
  functionName: "transfer",
225313
225404
  args: [bridgeEvm, amountRaw]
225314
225405
  });
225315
- let txHash;
225316
- if (getSigType() === 3) {
225317
- const res = await sendWalletBatch(
225318
- [{ target: PUSD_COLLATERAL, value: "0", data }],
225319
- owner,
225320
- "Withdraw"
225321
- );
225322
- txHash = res.transactionHash;
225323
- } else {
225324
- const account = getPolymarketAccount();
225325
- const wallet = createWalletClient({
225326
- account,
225327
- chain: polygon,
225328
- transport: http(POLYGON_RPC_URLS[0])
225329
- });
225330
- txHash = await wallet.sendTransaction({
225331
- to: PUSD_COLLATERAL,
225332
- data,
225333
- chain: polygon,
225334
- account
225335
- });
225336
- await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225337
- }
225406
+ const eoa = getSigType() !== 3;
225407
+ const txHash = await signUnderSpendPolicy(
225408
+ deps,
225409
+ {
225410
+ payTo: recipient,
225411
+ network: `eip155:${BASE_CHAIN_ID}`,
225412
+ asset: BASE_USDC,
225413
+ amount: amountRaw.toString()
225414
+ },
225415
+ "polymarket withdraw",
225416
+ async () => {
225417
+ if (!eoa) {
225418
+ const res = await sendWalletBatch(
225419
+ [{ target: PUSD_COLLATERAL, value: "0", data }],
225420
+ owner,
225421
+ "Withdraw"
225422
+ );
225423
+ return res.transactionHash;
225424
+ }
225425
+ const account = getPolymarketAccount();
225426
+ const wallet = createWalletClient({
225427
+ account,
225428
+ chain: polygon,
225429
+ transport: http(POLYGON_RPC_URLS[0])
225430
+ });
225431
+ return wallet.sendTransaction({
225432
+ to: PUSD_COLLATERAL,
225433
+ data,
225434
+ chain: polygon,
225435
+ account
225436
+ });
225437
+ }
225438
+ );
225439
+ if (eoa) await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225338
225440
  return {
225339
225441
  text: [
225340
225442
  `\u2705 Withdrawal submitted: $${amountUsd.toFixed(2)} pUSD \u2192 USDC on Base`,
@@ -225369,6 +225471,7 @@ var init_withdraw = __esm({
225369
225471
  init_positions();
225370
225472
  init_relayer();
225371
225473
  init_setup();
225474
+ init_spend_policy();
225372
225475
  }
225373
225476
  });
225374
225477
 
@@ -226014,7 +226117,7 @@ async function bridgeAddressFor(vault) {
226014
226117
  throw new Error(`Bridge did not return a deposit address (got: ${JSON.stringify(res.data)}).`);
226015
226118
  return evm;
226016
226119
  }
226017
- async function fundVault(input) {
226120
+ async function fundVault(input, deps) {
226018
226121
  if (input.amount_usd === void 0 || input.amount_usd <= 0) {
226019
226122
  return {
226020
226123
  text: `Pass amount_usd \u2014 the USDC amount to move from your Base wallet into your Polymarket vault (e.g. amount_usd:5).`,
@@ -226071,12 +226174,12 @@ async function fundVault(input) {
226071
226174
  }
226072
226175
  const privateKey = getOrCreateWalletKey();
226073
226176
  const amountMicro = String(Math.floor(amountUsd * 10 ** USDC_DECIMALS));
226074
- const depositAuthorization = await createPaymentPayload(
226075
- privateKey,
226076
- agent,
226077
- bridge,
226078
- amountMicro,
226079
- `eip155:${BASE_CHAIN_ID}`
226177
+ const network = `eip155:${BASE_CHAIN_ID}`;
226178
+ const depositAuthorization = await signUnderSpendPolicy(
226179
+ deps,
226180
+ { payTo: bridge, network, asset: BASE_USDC, amount: amountMicro },
226181
+ "polymarket fund",
226182
+ () => createPaymentPayload(privateKey, agent, bridge, amountMicro, network)
226080
226183
  );
226081
226184
  const client = new BlockrunClient({ privateKey });
226082
226185
  const result = await client.post("/v1/polymarket/fund", {
@@ -226126,6 +226229,7 @@ var init_fund = __esm({
226126
226229
  init_constants3();
226127
226230
  init_positions();
226128
226231
  init_setup();
226232
+ init_spend_policy();
226129
226233
  FUND_FEE_USD = 0.01;
226130
226234
  USDC_DECIMALS = 6;
226131
226235
  FUND_MIN_USD = Number(process.env.POLYMARKET_FUND_MIN_USD || "2");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.262",
4
- "description": "Smart LLM router — save 84% on inference costs. 76 models (7 free), one wallet, x402 micropayments.",
3
+ "version": "0.12.264",
4
+ "description": "Smart LLM router \u2014 save 84% on inference costs. 76 models (7 free), one wallet, x402 micropayments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",