@blockrun/clawrouter 0.12.263 → 0.12.265

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
@@ -117032,6 +117032,35 @@ var init_client5 = __esm({
117032
117032
  }
117033
117033
  });
117034
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
+
117035
117064
  // src/polymarket/orders.ts
117036
117065
  function getSessionLedger() {
117037
117066
  return {
@@ -117100,6 +117129,9 @@ function isCredsMismatchError(err) {
117100
117129
  return msg.includes("unauthorized") || msg.includes("api key not found") || msg.includes("invalid api key");
117101
117130
  }
117102
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
+ }
117103
117135
  const e7 = err;
117104
117136
  const dataText = e7?.data ? ` \u2014 ${typeof e7.data === "string" ? e7.data : JSON.stringify(e7.data)}` : "";
117105
117137
  const message = `${e7?.message ?? String(err)}${dataText}`;
@@ -117146,7 +117178,7 @@ async function withCredsRetry(fn) {
117146
117178
  return fn(fresh);
117147
117179
  }
117148
117180
  }
117149
- async function executeTrade(input) {
117181
+ async function executeTrade(input, deps) {
117150
117182
  const side = input.action === "buy" ? Side.BUY : Side.SELL;
117151
117183
  const isLimit = input.price !== void 0;
117152
117184
  if (isLimit && input.size === void 0) {
@@ -117158,6 +117190,18 @@ async function executeTrade(input) {
117158
117190
  isError: true
117159
117191
  };
117160
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
+ }
117161
117205
  if (!isLimit && input.action === "sell" && input.size === void 0) {
117162
117206
  return { text: `Market sells need size (shares to sell).`, isError: true };
117163
117207
  }
@@ -117257,39 +117301,44 @@ Re-call with confirm:true to sign and submit.`,
117257
117301
  }
117258
117302
  const options = { tickSize, negRisk };
117259
117303
  reserveBet(notional, input.agent_id);
117260
- 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;
117261
117311
  try {
117262
- response = isLimit ? await clob.createAndPostOrder(
117263
- {
117264
- tokenID: token.tokenId,
117265
- price,
117266
- size: size5,
117267
- side,
117268
- ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117269
- },
117270
- options,
117271
- orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117272
- input.post_only ?? false
117273
- ) : await clob.createAndPostMarketOrder(
117274
- {
117275
- tokenID: token.tokenId,
117276
- amount: input.action === "buy" ? input.amount_usd : size5,
117277
- side,
117278
- orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117279
- },
117280
- options,
117281
- orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117282
- );
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
+ });
117283
117338
  } catch (submitErr) {
117284
117339
  releaseBet(notional, input.agent_id);
117285
117340
  throw submitErr;
117286
117341
  }
117287
- const r2 = response;
117288
- const placed = r2?.success !== false && (r2?.orderID || r2?.status === "matched");
117289
- if (!placed) {
117290
- releaseBet(notional, input.agent_id);
117291
- throw new Error(r2?.errorMsg || "order rejected");
117292
- }
117293
117342
  commitBet();
117294
117343
  const filled = r2?.status === "matched" || (r2?.transactionsHashes?.length ?? 0) > 0;
117295
117344
  return {
@@ -117378,6 +117427,8 @@ var init_orders = __esm({
117378
117427
  init_client5();
117379
117428
  init_constants3();
117380
117429
  init_creds();
117430
+ init_spend_control();
117431
+ init_spend_policy();
117381
117432
  ledger = {
117382
117433
  totalUsd: 0,
117383
117434
  count: 0,
@@ -225312,7 +225363,7 @@ var init_setup = __esm({
225312
225363
  });
225313
225364
 
225314
225365
  // src/polymarket/redeem.ts
225315
- async function redeemPosition(input) {
225366
+ async function redeemPosition(input, deps) {
225316
225367
  if (!input.condition_id) {
225317
225368
  return {
225318
225369
  text: `Pass condition_id:"0x\u2026" (see action:"positions" for redeemable markets).`,
@@ -225388,20 +225439,31 @@ async function redeemPosition(input) {
225388
225439
  ]
225389
225440
  });
225390
225441
  const target = negRisk ? NEG_RISK_ADAPTER : CONDITIONAL_TOKENS;
225391
- let txHash;
225392
- if (getSigType() === 3) {
225393
- const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225394
- txHash = res.transactionHash;
225395
- } else {
225396
- const account = getPolymarketAccount();
225397
- const wallet = createWalletClient({
225398
- account,
225399
- chain: polygon,
225400
- transport: http(POLYGON_RPC_URLS[0])
225401
- });
225402
- txHash = await wallet.sendTransaction({ to: target, data, chain: polygon, account });
225403
- await pc.waitForTransactionReceipt({ hash: txHash });
225404
- }
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 });
225405
225467
  const balanceAfter = await getPusdBalance(owner).catch(() => null);
225406
225468
  return {
225407
225469
  text: [
@@ -225432,6 +225494,7 @@ var init_redeem = __esm({
225432
225494
  init_positions();
225433
225495
  init_relayer();
225434
225496
  init_setup();
225497
+ init_spend_policy();
225435
225498
  }
225436
225499
  });
225437
225500
 
@@ -225444,7 +225507,13 @@ async function rawPusdBalance(owner) {
225444
225507
  args: [owner]
225445
225508
  });
225446
225509
  }
225447
- 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
+ }
225448
225517
  let owner;
225449
225518
  try {
225450
225519
  owner = getFundsAddress();
@@ -225515,29 +225584,40 @@ async function withdrawFunds(input) {
225515
225584
  functionName: "transfer",
225516
225585
  args: [bridgeEvm, amountRaw]
225517
225586
  });
225518
- let txHash;
225519
- if (getSigType() === 3) {
225520
- const res = await sendWalletBatch(
225521
- [{ target: PUSD_COLLATERAL, value: "0", data }],
225522
- owner,
225523
- "Withdraw"
225524
- );
225525
- txHash = res.transactionHash;
225526
- } else {
225527
- const account = getPolymarketAccount();
225528
- const wallet = createWalletClient({
225529
- account,
225530
- chain: polygon,
225531
- transport: http(POLYGON_RPC_URLS[0])
225532
- });
225533
- txHash = await wallet.sendTransaction({
225534
- to: PUSD_COLLATERAL,
225535
- data,
225536
- chain: polygon,
225537
- account
225538
- });
225539
- await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225540
- }
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 });
225541
225621
  return {
225542
225622
  text: [
225543
225623
  `\u2705 Withdrawal submitted: $${amountUsd.toFixed(2)} pUSD \u2192 USDC on Base`,
@@ -225572,6 +225652,7 @@ var init_withdraw = __esm({
225572
225652
  init_positions();
225573
225653
  init_relayer();
225574
225654
  init_setup();
225655
+ init_spend_policy();
225575
225656
  }
225576
225657
  });
225577
225658
 
@@ -226217,7 +226298,7 @@ async function bridgeAddressFor(vault) {
226217
226298
  throw new Error(`Bridge did not return a deposit address (got: ${JSON.stringify(res.data)}).`);
226218
226299
  return evm;
226219
226300
  }
226220
- async function fundVault(input) {
226301
+ async function fundVault(input, deps) {
226221
226302
  if (input.amount_usd === void 0 || input.amount_usd <= 0) {
226222
226303
  return {
226223
226304
  text: `Pass amount_usd \u2014 the USDC amount to move from your Base wallet into your Polymarket vault (e.g. amount_usd:5).`,
@@ -226274,12 +226355,12 @@ async function fundVault(input) {
226274
226355
  }
226275
226356
  const privateKey = getOrCreateWalletKey();
226276
226357
  const amountMicro = String(Math.floor(amountUsd * 10 ** USDC_DECIMALS));
226277
- const depositAuthorization = await createPaymentPayload(
226278
- privateKey,
226279
- agent,
226280
- bridge,
226281
- amountMicro,
226282
- `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)
226283
226364
  );
226284
226365
  const client = new BlockrunClient({ privateKey });
226285
226366
  const result = await client.post("/v1/polymarket/fund", {
@@ -226329,6 +226410,7 @@ var init_fund = __esm({
226329
226410
  init_constants3();
226330
226411
  init_positions();
226331
226412
  init_setup();
226413
+ init_spend_policy();
226332
226414
  FUND_FEE_USD = 0.01;
226333
226415
  USDC_DECIMALS = 6;
226334
226416
  FUND_MIN_USD = Number(process.env.POLYMARKET_FUND_MIN_USD || "2");
@@ -226753,6 +226835,7 @@ var index_exports = {};
226753
226835
  __export(index_exports, {
226754
226836
  BALANCE_THRESHOLDS: () => BALANCE_THRESHOLDS,
226755
226837
  BLOCKRUN_MODELS: () => BLOCKRUN_MODELS,
226838
+ BLOCKRUN_PLUGIN_ID: () => BLOCKRUN_PLUGIN_ID,
226756
226839
  BalanceMonitor: () => BalanceMonitor,
226757
226840
  CAIP2_BASE: () => CAIP2_BASE,
226758
226841
  CAIP2_SOLANA_MAINNET: () => CAIP2_SOLANA_MAINNET,
@@ -227067,6 +227150,35 @@ function injectModelsConfig(logger48, options = {}) {
227067
227150
  logger48.info(`Added ${addedCount} models to allowlist (${TOP_MODELS.length} total)`);
227068
227151
  }
227069
227152
  }
227153
+ const pluginEntries = config.plugins?.entries;
227154
+ if (pluginEntries && pluginEntries.clawrouter && !pluginEntries[BLOCKRUN_PLUGIN_ID]) {
227155
+ pluginEntries[BLOCKRUN_PLUGIN_ID] = pluginEntries.clawrouter;
227156
+ delete pluginEntries.clawrouter;
227157
+ needsWrite = true;
227158
+ logger48.info(
227159
+ "Migrated plugins.entries.clawrouter -> blockrun-clawrouter (OpenClaw bundles its own `clawrouter` plugin; see #305)"
227160
+ );
227161
+ }
227162
+ const pluginsCfg = config.plugins;
227163
+ const allow = pluginsCfg?.allow;
227164
+ if (Array.isArray(allow) && allow.includes("clawrouter") && !allow.includes(BLOCKRUN_PLUGIN_ID)) {
227165
+ allow.push(BLOCKRUN_PLUGIN_ID);
227166
+ needsWrite = true;
227167
+ logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.allow (renamed from clawrouter; see #305)`);
227168
+ }
227169
+ const deny = pluginsCfg?.deny;
227170
+ if (Array.isArray(deny) && deny.includes("clawrouter") && !deny.includes(BLOCKRUN_PLUGIN_ID)) {
227171
+ deny.push(BLOCKRUN_PLUGIN_ID);
227172
+ needsWrite = true;
227173
+ logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.deny (renamed from clawrouter; see #305)`);
227174
+ }
227175
+ const installs = pluginsCfg?.installs;
227176
+ if (installs && installs.clawrouter && !installs[BLOCKRUN_PLUGIN_ID]) {
227177
+ installs[BLOCKRUN_PLUGIN_ID] = installs.clawrouter;
227178
+ delete installs.clawrouter;
227179
+ needsWrite = true;
227180
+ logger48.info(`Migrated plugins.installs.clawrouter -> ${BLOCKRUN_PLUGIN_ID} (see #305)`);
227181
+ }
227070
227182
  if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
227071
227183
  config.tools = {};
227072
227184
  needsWrite = true;
@@ -228059,7 +228171,7 @@ Run \`openclaw plugins install @blockrun/clawrouter\` to generate a wallet.`,
228059
228171
  }
228060
228172
  };
228061
228173
  }
228062
- var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
228174
+ var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, BLOCKRUN_PLUGIN_ID, plugin, index_default2;
228063
228175
  var init_index = __esm({
228064
228176
  "src/index.ts"() {
228065
228177
  "use strict";
@@ -228102,9 +228214,16 @@ var init_index = __esm({
228102
228214
  IMAGE_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "images");
228103
228215
  AUDIO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "audio");
228104
228216
  VIDEO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "videos");
228217
+ BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
228105
228218
  plugin = {
228106
- id: "clawrouter",
228107
- name: "ClawRouter",
228219
+ // NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
228220
+ // 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
228221
+ // CLAWROUTER_API_KEY, clawrouter.openclaw.ai). A duplicate id loses to the
228222
+ // bundled one — "global plugin will be overridden by bundled plugin" — so this
228223
+ // plugin silently never loaded and the local proxy never started. Different
228224
+ // product, different id. See #305.
228225
+ id: BLOCKRUN_PLUGIN_ID,
228226
+ name: "BlockRun ClawRouter",
228108
228227
  description: "Smart LLM router \u2014 55+ models, x402 micropayments, 78% cost savings",
228109
228228
  version: VERSION,
228110
228229
  register(api) {
@@ -228513,13 +228632,18 @@ ${errText}`
228513
228632
  delete config.models.providers.blockrun;
228514
228633
  }
228515
228634
  removeManagedBlockrunMcpServerConfig(config);
228516
- for (const key2 of ["clawrouter", "ClawRouter", "@blockrun/clawrouter"]) {
228635
+ for (const key2 of [
228636
+ BLOCKRUN_PLUGIN_ID,
228637
+ "clawrouter",
228638
+ "ClawRouter",
228639
+ "@blockrun/clawrouter"
228640
+ ]) {
228517
228641
  if (config.plugins?.entries?.[key2]) delete config.plugins.entries[key2];
228518
228642
  if (config.plugins?.installs?.[key2]) delete config.plugins.installs[key2];
228519
228643
  }
228520
228644
  if (Array.isArray(config.plugins?.allow)) {
228521
228645
  config.plugins.allow = config.plugins.allow.filter(
228522
- (p) => p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
228646
+ (p) => p !== BLOCKRUN_PLUGIN_ID && p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
228523
228647
  );
228524
228648
  }
228525
228649
  if (config.agents?.defaults?.models) {
@@ -228628,6 +228752,7 @@ init_balance();
228628
228752
  init_wallet2();
228629
228753
  init_stats();
228630
228754
  init_proxy();
228755
+ init_spend_control();
228631
228756
  init_version4();
228632
228757
  import { platform, arch, freemem, totalmem } from "os";
228633
228758
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
@@ -228894,6 +229019,15 @@ var DOCTOR_MODELS = {
228894
229019
  cost: "~$0.01"
228895
229020
  }
228896
229021
  };
229022
+ function createDoctorX402Client(opts) {
229023
+ const account = privateKeyToAccount(opts.walletKey);
229024
+ const publicClient = createPublicClient({ chain: base, transport: http() });
229025
+ const evmSigner = toClientEvmSigner(account, publicClient);
229026
+ const x402 = new x402Client();
229027
+ registerSpendPolicyHook(x402, opts.spendControl ?? new SpendControl());
229028
+ registerExactEvmScheme(x402, { signer: evmSigner });
229029
+ return x402;
229030
+ }
228897
229031
  async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
228898
229032
  if (diagnostics.wallet.isEmpty) {
228899
229033
  console.log("\n\u{1F4B3} Wallet is empty - cannot call AI for analysis.");
@@ -228911,11 +229045,7 @@ async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
228911
229045
  `);
228912
229046
  try {
228913
229047
  const { key: key2 } = await resolveOrGenerateWalletKey();
228914
- const account = privateKeyToAccount(key2);
228915
- const publicClient = createPublicClient({ chain: base, transport: http() });
228916
- const evmSigner = toClientEvmSigner(account, publicClient);
228917
- const x402 = new x402Client();
228918
- registerExactEvmScheme(x402, { signer: evmSigner });
229048
+ const x402 = createDoctorX402Client({ walletKey: key2 });
228919
229049
  const paymentChain = diagnostics.wallet.paymentChain;
228920
229050
  if (paymentChain === "solana") {
228921
229051
  try {
package/dist/index.d.ts CHANGED
@@ -1729,6 +1729,13 @@ declare function parseCallArgs(raw: string): {
1729
1729
  * Delegates to the local proxy (which handles x402 payment).
1730
1730
  */
1731
1731
  declare function buildImageGenerationProvider(): ImageGenerationProviderPlugin;
1732
+ /**
1733
+ * This plugin's OpenClaw id. NOT "clawrouter" — OpenClaw bundles its own plugin
1734
+ * under that id since the 2026.7.1 line, and a duplicate loses to the bundled
1735
+ * one. Kept as one constant so the manifest, the plugin definition and the
1736
+ * config migration can never drift apart. See #305.
1737
+ */
1738
+ declare const BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
1732
1739
  declare const plugin: OpenClawPluginDefinition;
1733
1740
 
1734
- export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
1741
+ export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, BLOCKRUN_PLUGIN_ID, type BalanceInfo, BalanceMonitor, CAIP2_BASE, CAIP2_SOLANA_MAINNET, type CachedLLMResponse, type CachedResponse, type CheckResult, type CounterpartyInfo, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, MalformedSpendPolicyError, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type PolicyList, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, SpendPolicyError, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildImageGenerationProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, registerSpendPolicyHook, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
package/dist/index.js CHANGED
@@ -116851,6 +116851,35 @@ var init_client5 = __esm({
116851
116851
  }
116852
116852
  });
116853
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
+
116854
116883
  // src/polymarket/orders.ts
116855
116884
  function getSessionLedger() {
116856
116885
  return {
@@ -116919,6 +116948,9 @@ function isCredsMismatchError(err) {
116919
116948
  return msg.includes("unauthorized") || msg.includes("api key not found") || msg.includes("invalid api key");
116920
116949
  }
116921
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
+ }
116922
116954
  const e7 = err;
116923
116955
  const dataText = e7?.data ? ` \u2014 ${typeof e7.data === "string" ? e7.data : JSON.stringify(e7.data)}` : "";
116924
116956
  const message = `${e7?.message ?? String(err)}${dataText}`;
@@ -116965,7 +116997,7 @@ async function withCredsRetry(fn) {
116965
116997
  return fn(fresh);
116966
116998
  }
116967
116999
  }
116968
- async function executeTrade(input) {
117000
+ async function executeTrade(input, deps) {
116969
117001
  const side = input.action === "buy" ? Side.BUY : Side.SELL;
116970
117002
  const isLimit = input.price !== void 0;
116971
117003
  if (isLimit && input.size === void 0) {
@@ -116977,6 +117009,18 @@ async function executeTrade(input) {
116977
117009
  isError: true
116978
117010
  };
116979
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
+ }
116980
117024
  if (!isLimit && input.action === "sell" && input.size === void 0) {
116981
117025
  return { text: `Market sells need size (shares to sell).`, isError: true };
116982
117026
  }
@@ -117076,39 +117120,44 @@ Re-call with confirm:true to sign and submit.`,
117076
117120
  }
117077
117121
  const options = { tickSize, negRisk };
117078
117122
  reserveBet(notional, input.agent_id);
117079
- 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;
117080
117130
  try {
117081
- response = isLimit ? await clob.createAndPostOrder(
117082
- {
117083
- tokenID: token.tokenId,
117084
- price,
117085
- size: size5,
117086
- side,
117087
- ...orderKind === "GTD" && input.expires_at ? { expiration: input.expires_at } : {}
117088
- },
117089
- options,
117090
- orderKind === "GTD" ? OrderType.GTD : OrderType.GTC,
117091
- input.post_only ?? false
117092
- ) : await clob.createAndPostMarketOrder(
117093
- {
117094
- tokenID: token.tokenId,
117095
- amount: input.action === "buy" ? input.amount_usd : size5,
117096
- side,
117097
- orderType: orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117098
- },
117099
- options,
117100
- orderKind === "FAK" ? OrderType.FAK : OrderType.FOK
117101
- );
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
+ });
117102
117157
  } catch (submitErr) {
117103
117158
  releaseBet(notional, input.agent_id);
117104
117159
  throw submitErr;
117105
117160
  }
117106
- const r2 = response;
117107
- const placed = r2?.success !== false && (r2?.orderID || r2?.status === "matched");
117108
- if (!placed) {
117109
- releaseBet(notional, input.agent_id);
117110
- throw new Error(r2?.errorMsg || "order rejected");
117111
- }
117112
117161
  commitBet();
117113
117162
  const filled = r2?.status === "matched" || (r2?.transactionsHashes?.length ?? 0) > 0;
117114
117163
  return {
@@ -117197,6 +117246,8 @@ var init_orders = __esm({
117197
117246
  init_client5();
117198
117247
  init_constants3();
117199
117248
  init_creds();
117249
+ init_spend_control();
117250
+ init_spend_policy();
117200
117251
  ledger = {
117201
117252
  totalUsd: 0,
117202
117253
  count: 0,
@@ -225131,7 +225182,7 @@ var init_setup = __esm({
225131
225182
  });
225132
225183
 
225133
225184
  // src/polymarket/redeem.ts
225134
- async function redeemPosition(input) {
225185
+ async function redeemPosition(input, deps) {
225135
225186
  if (!input.condition_id) {
225136
225187
  return {
225137
225188
  text: `Pass condition_id:"0x\u2026" (see action:"positions" for redeemable markets).`,
@@ -225207,20 +225258,31 @@ async function redeemPosition(input) {
225207
225258
  ]
225208
225259
  });
225209
225260
  const target = negRisk ? NEG_RISK_ADAPTER : CONDITIONAL_TOKENS;
225210
- let txHash;
225211
- if (getSigType() === 3) {
225212
- const res = await sendWalletBatch([{ target, value: "0", data }], owner, "Redeem");
225213
- txHash = res.transactionHash;
225214
- } else {
225215
- const account = getPolymarketAccount();
225216
- const wallet = createWalletClient({
225217
- account,
225218
- chain: polygon,
225219
- transport: http(POLYGON_RPC_URLS[0])
225220
- });
225221
- txHash = await wallet.sendTransaction({ to: target, data, chain: polygon, account });
225222
- await pc.waitForTransactionReceipt({ hash: txHash });
225223
- }
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 });
225224
225286
  const balanceAfter = await getPusdBalance(owner).catch(() => null);
225225
225287
  return {
225226
225288
  text: [
@@ -225251,6 +225313,7 @@ var init_redeem = __esm({
225251
225313
  init_positions();
225252
225314
  init_relayer();
225253
225315
  init_setup();
225316
+ init_spend_policy();
225254
225317
  }
225255
225318
  });
225256
225319
 
@@ -225263,7 +225326,13 @@ async function rawPusdBalance(owner) {
225263
225326
  args: [owner]
225264
225327
  });
225265
225328
  }
225266
- 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
+ }
225267
225336
  let owner;
225268
225337
  try {
225269
225338
  owner = getFundsAddress();
@@ -225334,29 +225403,40 @@ async function withdrawFunds(input) {
225334
225403
  functionName: "transfer",
225335
225404
  args: [bridgeEvm, amountRaw]
225336
225405
  });
225337
- let txHash;
225338
- if (getSigType() === 3) {
225339
- const res = await sendWalletBatch(
225340
- [{ target: PUSD_COLLATERAL, value: "0", data }],
225341
- owner,
225342
- "Withdraw"
225343
- );
225344
- txHash = res.transactionHash;
225345
- } else {
225346
- const account = getPolymarketAccount();
225347
- const wallet = createWalletClient({
225348
- account,
225349
- chain: polygon,
225350
- transport: http(POLYGON_RPC_URLS[0])
225351
- });
225352
- txHash = await wallet.sendTransaction({
225353
- to: PUSD_COLLATERAL,
225354
- data,
225355
- chain: polygon,
225356
- account
225357
- });
225358
- await getPublicClient().waitForTransactionReceipt({ hash: txHash });
225359
- }
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 });
225360
225440
  return {
225361
225441
  text: [
225362
225442
  `\u2705 Withdrawal submitted: $${amountUsd.toFixed(2)} pUSD \u2192 USDC on Base`,
@@ -225391,6 +225471,7 @@ var init_withdraw = __esm({
225391
225471
  init_positions();
225392
225472
  init_relayer();
225393
225473
  init_setup();
225474
+ init_spend_policy();
225394
225475
  }
225395
225476
  });
225396
225477
 
@@ -226036,7 +226117,7 @@ async function bridgeAddressFor(vault) {
226036
226117
  throw new Error(`Bridge did not return a deposit address (got: ${JSON.stringify(res.data)}).`);
226037
226118
  return evm;
226038
226119
  }
226039
- async function fundVault(input) {
226120
+ async function fundVault(input, deps) {
226040
226121
  if (input.amount_usd === void 0 || input.amount_usd <= 0) {
226041
226122
  return {
226042
226123
  text: `Pass amount_usd \u2014 the USDC amount to move from your Base wallet into your Polymarket vault (e.g. amount_usd:5).`,
@@ -226093,12 +226174,12 @@ async function fundVault(input) {
226093
226174
  }
226094
226175
  const privateKey = getOrCreateWalletKey();
226095
226176
  const amountMicro = String(Math.floor(amountUsd * 10 ** USDC_DECIMALS));
226096
- const depositAuthorization = await createPaymentPayload(
226097
- privateKey,
226098
- agent,
226099
- bridge,
226100
- amountMicro,
226101
- `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)
226102
226183
  );
226103
226184
  const client = new BlockrunClient({ privateKey });
226104
226185
  const result = await client.post("/v1/polymarket/fund", {
@@ -226148,6 +226229,7 @@ var init_fund = __esm({
226148
226229
  init_constants3();
226149
226230
  init_positions();
226150
226231
  init_setup();
226232
+ init_spend_policy();
226151
226233
  FUND_FEE_USD = 0.01;
226152
226234
  USDC_DECIMALS = 6;
226153
226235
  FUND_MIN_USD = Number(process.env.POLYMARKET_FUND_MIN_USD || "2");
@@ -226813,6 +226895,35 @@ function injectModelsConfig(logger48, options = {}) {
226813
226895
  logger48.info(`Added ${addedCount} models to allowlist (${TOP_MODELS.length} total)`);
226814
226896
  }
226815
226897
  }
226898
+ const pluginEntries = config.plugins?.entries;
226899
+ if (pluginEntries && pluginEntries.clawrouter && !pluginEntries[BLOCKRUN_PLUGIN_ID]) {
226900
+ pluginEntries[BLOCKRUN_PLUGIN_ID] = pluginEntries.clawrouter;
226901
+ delete pluginEntries.clawrouter;
226902
+ needsWrite = true;
226903
+ logger48.info(
226904
+ "Migrated plugins.entries.clawrouter -> blockrun-clawrouter (OpenClaw bundles its own `clawrouter` plugin; see #305)"
226905
+ );
226906
+ }
226907
+ const pluginsCfg = config.plugins;
226908
+ const allow = pluginsCfg?.allow;
226909
+ if (Array.isArray(allow) && allow.includes("clawrouter") && !allow.includes(BLOCKRUN_PLUGIN_ID)) {
226910
+ allow.push(BLOCKRUN_PLUGIN_ID);
226911
+ needsWrite = true;
226912
+ logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.allow (renamed from clawrouter; see #305)`);
226913
+ }
226914
+ const deny = pluginsCfg?.deny;
226915
+ if (Array.isArray(deny) && deny.includes("clawrouter") && !deny.includes(BLOCKRUN_PLUGIN_ID)) {
226916
+ deny.push(BLOCKRUN_PLUGIN_ID);
226917
+ needsWrite = true;
226918
+ logger48.info(`Added ${BLOCKRUN_PLUGIN_ID} to plugins.deny (renamed from clawrouter; see #305)`);
226919
+ }
226920
+ const installs = pluginsCfg?.installs;
226921
+ if (installs && installs.clawrouter && !installs[BLOCKRUN_PLUGIN_ID]) {
226922
+ installs[BLOCKRUN_PLUGIN_ID] = installs.clawrouter;
226923
+ delete installs.clawrouter;
226924
+ needsWrite = true;
226925
+ logger48.info(`Migrated plugins.installs.clawrouter -> ${BLOCKRUN_PLUGIN_ID} (see #305)`);
226926
+ }
226816
226927
  if (!config.tools || typeof config.tools !== "object" || Array.isArray(config.tools)) {
226817
226928
  config.tools = {};
226818
226929
  needsWrite = true;
@@ -227805,7 +227916,7 @@ Run \`openclaw plugins install @blockrun/clawrouter\` to generate a wallet.`,
227805
227916
  }
227806
227917
  };
227807
227918
  }
227808
- var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, plugin, index_default2;
227919
+ var activeProxyHandle, pendingConfiguredStartupApi, IMAGE_DIR2, AUDIO_DIR2, VIDEO_DIR2, BLOCKRUN_PLUGIN_ID, plugin, index_default2;
227809
227920
  var init_index = __esm({
227810
227921
  "src/index.ts"() {
227811
227922
  init_provider();
@@ -227847,9 +227958,16 @@ var init_index = __esm({
227847
227958
  IMAGE_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "images");
227848
227959
  AUDIO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "audio");
227849
227960
  VIDEO_DIR2 = join16(homedir13(), ".openclaw", "blockrun", "videos");
227961
+ BLOCKRUN_PLUGIN_ID = "blockrun-clawrouter";
227850
227962
  plugin = {
227851
- id: "clawrouter",
227852
- name: "ClawRouter",
227963
+ // NOT "clawrouter". OpenClaw bundles its own plugin under that id since the
227964
+ // 2026.7.1 line (vendored at dist/extensions/clawrouter, provider `clawrouter/*`,
227965
+ // CLAWROUTER_API_KEY, clawrouter.openclaw.ai). A duplicate id loses to the
227966
+ // bundled one — "global plugin will be overridden by bundled plugin" — so this
227967
+ // plugin silently never loaded and the local proxy never started. Different
227968
+ // product, different id. See #305.
227969
+ id: BLOCKRUN_PLUGIN_ID,
227970
+ name: "BlockRun ClawRouter",
227853
227971
  description: "Smart LLM router \u2014 55+ models, x402 micropayments, 78% cost savings",
227854
227972
  version: VERSION,
227855
227973
  register(api) {
@@ -228258,13 +228376,18 @@ ${errText}`
228258
228376
  delete config.models.providers.blockrun;
228259
228377
  }
228260
228378
  removeManagedBlockrunMcpServerConfig(config);
228261
- for (const key2 of ["clawrouter", "ClawRouter", "@blockrun/clawrouter"]) {
228379
+ for (const key2 of [
228380
+ BLOCKRUN_PLUGIN_ID,
228381
+ "clawrouter",
228382
+ "ClawRouter",
228383
+ "@blockrun/clawrouter"
228384
+ ]) {
228262
228385
  if (config.plugins?.entries?.[key2]) delete config.plugins.entries[key2];
228263
228386
  if (config.plugins?.installs?.[key2]) delete config.plugins.installs[key2];
228264
228387
  }
228265
228388
  if (Array.isArray(config.plugins?.allow)) {
228266
228389
  config.plugins.allow = config.plugins.allow.filter(
228267
- (p) => p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
228390
+ (p) => p !== BLOCKRUN_PLUGIN_ID && p !== "clawrouter" && p !== "ClawRouter" && p !== "@blockrun/clawrouter"
228268
228391
  );
228269
228392
  }
228270
228393
  if (config.agents?.defaults?.models) {
@@ -228315,6 +228438,7 @@ init_index();
228315
228438
  export {
228316
228439
  BALANCE_THRESHOLDS,
228317
228440
  BLOCKRUN_MODELS,
228441
+ BLOCKRUN_PLUGIN_ID,
228318
228442
  BalanceMonitor,
228319
228443
  CAIP2_BASE,
228320
228444
  CAIP2_SOLANA_MAINNET,
@@ -1,7 +1,7 @@
1
1
  {
2
- "id": "clawrouter",
3
- "name": "ClawRouter",
4
- "description": "Smart LLM router — 55+ models, x402 micropayments, 92% cost savings. Phone lookup + AI voice calls (Twilio + Bland.ai). Surf crypto-data API (84 endpoints, on-chain SQL, 100M+ labeled wallets) via skill.",
2
+ "id": "blockrun-clawrouter",
3
+ "name": "BlockRun ClawRouter",
4
+ "description": "Smart LLM router \u2014 55+ models, x402 micropayments, 92% cost savings. Phone lookup + AI voice calls (Twilio + Bland.ai). Surf crypto-data API (84 endpoints, on-chain SQL, 100M+ labeled wallets) via skill.",
5
5
  "skills": ["./skills"],
6
6
  "activation": {
7
7
  "onStartup": true
@@ -44,7 +44,7 @@
44
44
  "properties": {
45
45
  "walletKey": {
46
46
  "type": "string",
47
- "description": "EVM wallet private key (0x...). Optional — auto-generated if not set."
47
+ "description": "EVM wallet private key (0x...). Optional \u2014 auto-generated if not set."
48
48
  },
49
49
  "routing": {
50
50
  "type": "object",
@@ -56,7 +56,7 @@
56
56
  "walletKey": {
57
57
  "label": "Wallet Private Key",
58
58
  "sensitive": true,
59
- "placeholder": "0x... (optional — auto-generated)"
59
+ "placeholder": "0x... (optional \u2014 auto-generated)"
60
60
  }
61
61
  }
62
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.263",
3
+ "version": "0.12.265",
4
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",
@@ -34,6 +34,7 @@
34
34
  "docs/*.png",
35
35
  "scripts",
36
36
  "!scripts/smoke-dist.mjs",
37
+ "!scripts/sync-brand-numbers.mjs",
37
38
  "skills",
38
39
  "src/top-models.json",
39
40
  "openclaw.plugin.json"
@@ -1,270 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Sync marketing numbers from BlockRun's canonical brand artifact.
4
- *
5
- * This file is copied byte-for-byte into every public repo as
6
- * scripts/sync-brand-numbers.mjs. It is a copy rather than an npm package on
7
- * purpose: a package would mean 37 dependency bumps, and several consuming
8
- * repos have no package.json at all. Zero dependencies, plain Node.
9
- *
10
- * node scripts/sync-brand-numbers.mjs rewrite markers in place
11
- * node scripts/sync-brand-numbers.mjs --check exit 1 on drift, write nothing
12
- * node scripts/sync-brand-numbers.mjs --refresh re-fetch the artifact first
13
- *
14
- * --check NEVER touches the network. PR CI must be deterministic and offline:
15
- * if it fetched, a deploy in progress would fail every repo in the org at once.
16
- * Freshness is the fan-out job's problem, not the pull request's.
17
- *
18
- * Markers look like: <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible -->
19
- * and wrap the WHOLE token, so a badge URL, its alt text and the prose number
20
- * can all regenerate from one key.
21
- */
22
- import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
23
- import { join, relative, extname } from "node:path";
24
-
25
- const ROOT = process.cwd();
26
- const SNAPSHOT = join(ROOT, "brand-numbers.json");
27
- // ORIGIN is tried first because it IS the truth — the mirror can only ever be
28
- // as fresh as the last time someone refreshed it. The mirror exists so a repo
29
- // can still sync while blockrun.ai is down, not to front the origin.
30
- //
31
- // The mirror is awesome-blockrun's own brand-numbers.json: that repo consumes
32
- // the artifact like every other, and its snapshot doubles as the org's copy.
33
- // One file, one role per repo, nothing to keep in step by hand.
34
- const ORIGIN = "https://blockrun.ai/brand/numbers.json";
35
- const MIRROR =
36
- "https://raw.githubusercontent.com/BlockRunAI/awesome-blockrun/main/brand-numbers.json";
37
-
38
- const argv = new Set(process.argv.slice(2));
39
- const check = argv.has("--check");
40
- const refresh = argv.has("--refresh");
41
-
42
- const SKIP_DIRS = new Set([
43
- "node_modules", ".git", "dist", "build", "out", ".next", "coverage",
44
- "vendor", "target", "__pycache__", ".venv", "venv",
45
- ]);
46
- // .txt is here for llms.txt, which is a first-class marketing surface: it is
47
- // what agents read to find out what BlockRun serves. Scanning other .txt files
48
- // costs a read and changes nothing — only files with markers are ever written.
49
- const TEXT_EXT = new Set([".md", ".mdx", ".txt"]);
50
-
51
- /* ── 1. numbers ──────────────────────────────────────────────────────────── */
52
-
53
- async function loadNumbers() {
54
- if (!refresh) {
55
- try {
56
- return JSON.parse(readFileSync(SNAPSHOT, "utf8"));
57
- } catch {
58
- fail(
59
- `no brand-numbers.json in ${ROOT}\n` +
60
- ` run with --refresh once to seed it from ${ORIGIN}`,
61
- );
62
- }
63
- }
64
- for (const url of [ORIGIN, MIRROR]) {
65
- try {
66
- const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
67
- if (!res.ok) continue;
68
- const json = await res.json();
69
- writeFileSync(SNAPSHOT, `${JSON.stringify(json, null, 2)}\n`);
70
- return json;
71
- } catch {
72
- /* try the next source */
73
- }
74
- }
75
- fail(`could not refresh from ${MIRROR} or ${ORIGIN}`);
76
- }
77
-
78
- /** Flatten nested numbers into dotted keys, ignoring $comment / rationale prose. */
79
- function flatten(obj, prefix = "") {
80
- return Object.entries(obj).flatMap(([k, v]) => {
81
- if (k.startsWith("$")) return [];
82
- const key = `${prefix}${k}`;
83
- if (v && typeof v === "object" && !Array.isArray(v)) return flatten(v, `${key}.`);
84
- if (v === null) return [];
85
- return [[key, v]];
86
- });
87
- }
88
-
89
- /* ── 2. renderers ────────────────────────────────────────────────────────── */
90
-
91
- /**
92
- * How a key becomes text. Default is the bare value.
93
- *
94
- * A marker may carry an `@modifier` — `<!-- br:mcp.tools@badge -->` — which
95
- * selects a renderer without changing which number is looked up. The modifier
96
- * is what makes a key reusable: the same mcp.tools appears as a shields badge
97
- * at the top of a README and as a bare "19 tools" in a table two screens down,
98
- * and one marker still keeps the badge URL, its alt text and the label in step.
99
- *
100
- * Renderers are registered under the FULL marker name so a badge's label is
101
- * written out rather than guessed from the key.
102
- */
103
- const badge = (label) => (n) =>
104
- `<img src="https://img.shields.io/badge/${label}-${n}-5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${n} ${label}">`;
105
-
106
- const RENDER = {
107
- "mcp.tools@badge": badge("tools"),
108
- "models.totalVisible@badge": badge("models"),
109
- "models.chatVisible@badge": badge("models"),
110
- };
111
- const render = (marker, value) => (RENDER[marker] ?? String)(value);
112
-
113
- /** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */
114
- const keyOf = (marker) => marker.split("@")[0];
115
-
116
- /* ── 3. marker rewriting ─────────────────────────────────────────────────── */
117
-
118
- const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
- const OPEN_ANY = /<!--\s*br:([A-Za-z0-9_.@]+)\s*-->/g;
120
- const CLOSE_ANY = /<!--\s*\/br:([A-Za-z0-9_.@]+)\s*-->/g;
121
-
122
- /** Byte ranges of fenced code blocks — markers inside them are documentation. */
123
- function fencedRanges(text) {
124
- const ranges = [];
125
- const fence = /^(\s*)(`{3,}|~{3,})[^\n]*$/gm;
126
- let open = null;
127
- for (let m; (m = fence.exec(text)); ) {
128
- if (open === null) open = m.index;
129
- else {
130
- ranges.push([open, m.index + m[0].length]);
131
- open = null;
132
- }
133
- }
134
- return ranges;
135
- }
136
-
137
- function syncFile(file, numbers, problems) {
138
- const before = readFileSync(file, "utf8");
139
- const rel = relative(ROOT, file);
140
- const fenced = fencedRanges(before);
141
- const inFence = (i) => fenced.some(([a, b]) => i >= a && i < b);
142
- const known = new Map(numbers);
143
- const used = new Set();
144
-
145
- // Markers actually present, so a file is only ever rewritten for what it uses
146
- // and an @modifier is carried through to the renderer verbatim.
147
- const markers = new Set();
148
- // A marker naming a key that does not exist is an error, never a silent
149
- // no-op: a typo'd marker would otherwise sit there looking synced forever.
150
- for (const [re, shown] of [
151
- [OPEN_ANY, (n) => `<!-- br:${n} -->`],
152
- [CLOSE_ANY, (n) => `<!-- /br:${n} -->`],
153
- ]) {
154
- for (const m of before.matchAll(re)) {
155
- if (inFence(m.index)) continue;
156
- markers.add(m[1]);
157
- if (!known.has(keyOf(m[1]))) problems.push(`${rel}: unknown key ${shown(m[1])}`);
158
- }
159
- }
160
-
161
- let after = before;
162
- for (const marker of markers) {
163
- const key = keyOf(marker);
164
- if (!known.has(key)) continue;
165
- const value = known.get(key);
166
- const pair = new RegExp(
167
- `(<!--\\s*br:${esc(marker)}\\s*-->)([\\s\\S]*?)(<!--\\s*/br:${esc(marker)}\\s*-->)`,
168
- "g",
169
- );
170
- after = after.replace(pair, (whole, open, inner, close, offset) => {
171
- if (inFence(offset)) return whole;
172
- // Nesting means the closing tag of an inner marker would be consumed by
173
- // the outer one. Refuse rather than produce mangled output.
174
- if (/<!--\s*\/?br:/.test(inner)) {
175
- problems.push(`${rel}: nested marker inside br:${marker}`);
176
- return whole;
177
- }
178
- used.add(marker);
179
- return open + render(marker, value) + close;
180
- });
181
-
182
- // An opening tag with no partner silently swallows the rest of the file on
183
- // a naive regex, so catch it explicitly.
184
- const opens = [...before.matchAll(new RegExp(`<!--\\s*br:${esc(marker)}\\s*-->`, "g"))]
185
- .filter((m) => !inFence(m.index)).length;
186
- const closes = [...before.matchAll(new RegExp(`<!--\\s*/br:${esc(marker)}\\s*-->`, "g"))]
187
- .filter((m) => !inFence(m.index)).length;
188
- if (opens !== closes) problems.push(`${rel}: unbalanced marker br:${marker} (${opens} open, ${closes} close)`);
189
- }
190
-
191
- return { before, after, changed: before !== after, used };
192
- }
193
-
194
- /* ── 4. walk ─────────────────────────────────────────────────────────────── */
195
-
196
- function* walk(dir) {
197
- for (const name of readdirSync(dir)) {
198
- if (SKIP_DIRS.has(name)) continue;
199
- const p = join(dir, name);
200
- // lstat, not stat: a symlinked directory is reached by its real path or not
201
- // at all. blockrun's docs/ -> awesome-blockrun/docs is exactly the case that
202
- // matters — following it would edit a submodule's files behind the skip
203
- // below, and a link pointing at an ancestor would recurse forever.
204
- const s = lstatSync(p);
205
- if (s.isSymbolicLink()) continue;
206
- if (s.isDirectory()) {
207
- // A nested repo is a submodule or vendored checkout: it carries its own
208
- // brand-numbers.json and syncs itself. Rewriting its markers from THIS
209
- // repo's snapshot would dirty a submodule nobody asked us to touch, and
210
- // would report drift that belongs to another repo's CI.
211
- if (existsSync(join(p, ".git"))) continue;
212
- yield* walk(p);
213
- } else if (TEXT_EXT.has(extname(name))) yield p;
214
- }
215
- }
216
-
217
- function fail(msg) {
218
- console.error(`brand-numbers: ${msg}`);
219
- process.exit(1);
220
- }
221
-
222
- /* ── 5. run ──────────────────────────────────────────────────────────────── */
223
-
224
- const raw = await loadNumbers();
225
- const numbers = flatten(raw);
226
- const problems = [];
227
- const drifted = [];
228
- const everUsed = new Set();
229
-
230
- for (const file of walk(ROOT)) {
231
- const { before, after, changed, used } = syncFile(file, numbers, problems);
232
- used.forEach((k) => everUsed.add(k));
233
- if (!changed) continue;
234
- drifted.push({ file: relative(ROOT, file), before, after });
235
- if (!check) writeFileSync(file, after);
236
- }
237
-
238
- if (problems.length) {
239
- for (const p of problems) console.error(` ${p}`);
240
- fail(`${problems.length} marker problem(s)`);
241
- }
242
-
243
- if (check) {
244
- if (drifted.length === 0) {
245
- console.log(`brand-numbers: up to date (${everUsed.size} keys in use)`);
246
- process.exit(0);
247
- }
248
- console.error("brand-numbers: these files disagree with brand-numbers.json\n");
249
- for (const { file, before, after } of drifted) {
250
- const b = before.split("\n");
251
- const a = after.split("\n");
252
- for (let i = 0; i < Math.max(b.length, a.length); i++) {
253
- if (b[i] !== a[i]) {
254
- console.error(` ${file}:${i + 1}`);
255
- console.error(` - ${(b[i] ?? "").trim()}`);
256
- console.error(` + ${(a[i] ?? "").trim()}`);
257
- }
258
- }
259
- }
260
- console.error(
261
- "\n fix with: node scripts/sync-brand-numbers.mjs && git commit -am 'chore: sync brand numbers'",
262
- );
263
- process.exit(1);
264
- }
265
-
266
- console.log(
267
- drifted.length
268
- ? `brand-numbers: updated ${drifted.length} file(s)`
269
- : `brand-numbers: already up to date (${everUsed.size} keys in use)`,
270
- );