@blockrun/clawrouter 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -95,40 +95,260 @@ var envKeyAuth = {
95
95
  // src/models.ts
96
96
  var BLOCKRUN_MODELS = [
97
97
  // Smart routing meta-model — proxy replaces with actual model
98
- { id: "blockrun/auto", name: "BlockRun Smart Router", inputPrice: 0, outputPrice: 0, contextWindow: 105e4, maxOutput: 128e3 },
98
+ {
99
+ id: "blockrun/auto",
100
+ name: "BlockRun Smart Router",
101
+ inputPrice: 0,
102
+ outputPrice: 0,
103
+ contextWindow: 105e4,
104
+ maxOutput: 128e3
105
+ },
99
106
  // OpenAI GPT-5 Family
100
- { id: "openai/gpt-5.2", name: "GPT-5.2", inputPrice: 1.75, outputPrice: 14, contextWindow: 4e5, maxOutput: 128e3, reasoning: true, vision: true },
101
- { id: "openai/gpt-5-mini", name: "GPT-5 Mini", inputPrice: 0.25, outputPrice: 2, contextWindow: 2e5, maxOutput: 65536 },
102
- { id: "openai/gpt-5-nano", name: "GPT-5 Nano", inputPrice: 0.05, outputPrice: 0.4, contextWindow: 128e3, maxOutput: 32768 },
103
- { id: "openai/gpt-5.2-pro", name: "GPT-5.2 Pro", inputPrice: 21, outputPrice: 168, contextWindow: 4e5, maxOutput: 128e3, reasoning: true },
107
+ {
108
+ id: "openai/gpt-5.2",
109
+ name: "GPT-5.2",
110
+ inputPrice: 1.75,
111
+ outputPrice: 14,
112
+ contextWindow: 4e5,
113
+ maxOutput: 128e3,
114
+ reasoning: true,
115
+ vision: true
116
+ },
117
+ {
118
+ id: "openai/gpt-5-mini",
119
+ name: "GPT-5 Mini",
120
+ inputPrice: 0.25,
121
+ outputPrice: 2,
122
+ contextWindow: 2e5,
123
+ maxOutput: 65536
124
+ },
125
+ {
126
+ id: "openai/gpt-5-nano",
127
+ name: "GPT-5 Nano",
128
+ inputPrice: 0.05,
129
+ outputPrice: 0.4,
130
+ contextWindow: 128e3,
131
+ maxOutput: 32768
132
+ },
133
+ {
134
+ id: "openai/gpt-5.2-pro",
135
+ name: "GPT-5.2 Pro",
136
+ inputPrice: 21,
137
+ outputPrice: 168,
138
+ contextWindow: 4e5,
139
+ maxOutput: 128e3,
140
+ reasoning: true
141
+ },
104
142
  // OpenAI GPT-4 Family
105
- { id: "openai/gpt-4.1", name: "GPT-4.1", inputPrice: 2, outputPrice: 8, contextWindow: 128e3, maxOutput: 16384, vision: true },
106
- { id: "openai/gpt-4.1-mini", name: "GPT-4.1 Mini", inputPrice: 0.4, outputPrice: 1.6, contextWindow: 128e3, maxOutput: 16384 },
107
- { id: "openai/gpt-4.1-nano", name: "GPT-4.1 Nano", inputPrice: 0.1, outputPrice: 0.4, contextWindow: 128e3, maxOutput: 16384 },
108
- { id: "openai/gpt-4o", name: "GPT-4o", inputPrice: 2.5, outputPrice: 10, contextWindow: 128e3, maxOutput: 16384, vision: true },
109
- { id: "openai/gpt-4o-mini", name: "GPT-4o Mini", inputPrice: 0.15, outputPrice: 0.6, contextWindow: 128e3, maxOutput: 16384 },
143
+ {
144
+ id: "openai/gpt-4.1",
145
+ name: "GPT-4.1",
146
+ inputPrice: 2,
147
+ outputPrice: 8,
148
+ contextWindow: 128e3,
149
+ maxOutput: 16384,
150
+ vision: true
151
+ },
152
+ {
153
+ id: "openai/gpt-4.1-mini",
154
+ name: "GPT-4.1 Mini",
155
+ inputPrice: 0.4,
156
+ outputPrice: 1.6,
157
+ contextWindow: 128e3,
158
+ maxOutput: 16384
159
+ },
160
+ {
161
+ id: "openai/gpt-4.1-nano",
162
+ name: "GPT-4.1 Nano",
163
+ inputPrice: 0.1,
164
+ outputPrice: 0.4,
165
+ contextWindow: 128e3,
166
+ maxOutput: 16384
167
+ },
168
+ {
169
+ id: "openai/gpt-4o",
170
+ name: "GPT-4o",
171
+ inputPrice: 2.5,
172
+ outputPrice: 10,
173
+ contextWindow: 128e3,
174
+ maxOutput: 16384,
175
+ vision: true
176
+ },
177
+ {
178
+ id: "openai/gpt-4o-mini",
179
+ name: "GPT-4o Mini",
180
+ inputPrice: 0.15,
181
+ outputPrice: 0.6,
182
+ contextWindow: 128e3,
183
+ maxOutput: 16384
184
+ },
110
185
  // OpenAI O-series (Reasoning)
111
- { id: "openai/o1", name: "o1", inputPrice: 15, outputPrice: 60, contextWindow: 2e5, maxOutput: 1e5, reasoning: true },
112
- { id: "openai/o1-mini", name: "o1-mini", inputPrice: 1.1, outputPrice: 4.4, contextWindow: 128e3, maxOutput: 65536, reasoning: true },
113
- { id: "openai/o3", name: "o3", inputPrice: 2, outputPrice: 8, contextWindow: 2e5, maxOutput: 1e5, reasoning: true },
114
- { id: "openai/o3-mini", name: "o3-mini", inputPrice: 1.1, outputPrice: 4.4, contextWindow: 128e3, maxOutput: 65536, reasoning: true },
115
- { id: "openai/o4-mini", name: "o4-mini", inputPrice: 1.1, outputPrice: 4.4, contextWindow: 128e3, maxOutput: 65536, reasoning: true },
186
+ {
187
+ id: "openai/o1",
188
+ name: "o1",
189
+ inputPrice: 15,
190
+ outputPrice: 60,
191
+ contextWindow: 2e5,
192
+ maxOutput: 1e5,
193
+ reasoning: true
194
+ },
195
+ {
196
+ id: "openai/o1-mini",
197
+ name: "o1-mini",
198
+ inputPrice: 1.1,
199
+ outputPrice: 4.4,
200
+ contextWindow: 128e3,
201
+ maxOutput: 65536,
202
+ reasoning: true
203
+ },
204
+ {
205
+ id: "openai/o3",
206
+ name: "o3",
207
+ inputPrice: 2,
208
+ outputPrice: 8,
209
+ contextWindow: 2e5,
210
+ maxOutput: 1e5,
211
+ reasoning: true
212
+ },
213
+ {
214
+ id: "openai/o3-mini",
215
+ name: "o3-mini",
216
+ inputPrice: 1.1,
217
+ outputPrice: 4.4,
218
+ contextWindow: 128e3,
219
+ maxOutput: 65536,
220
+ reasoning: true
221
+ },
222
+ {
223
+ id: "openai/o4-mini",
224
+ name: "o4-mini",
225
+ inputPrice: 1.1,
226
+ outputPrice: 4.4,
227
+ contextWindow: 128e3,
228
+ maxOutput: 65536,
229
+ reasoning: true
230
+ },
116
231
  // Anthropic
117
- { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", inputPrice: 1, outputPrice: 5, contextWindow: 2e5, maxOutput: 8192 },
118
- { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", inputPrice: 3, outputPrice: 15, contextWindow: 2e5, maxOutput: 64e3, reasoning: true },
119
- { id: "anthropic/claude-opus-4", name: "Claude Opus 4", inputPrice: 15, outputPrice: 75, contextWindow: 2e5, maxOutput: 32e3, reasoning: true },
120
- { id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5", inputPrice: 15, outputPrice: 75, contextWindow: 2e5, maxOutput: 32e3, reasoning: true },
232
+ {
233
+ id: "anthropic/claude-haiku-4.5",
234
+ name: "Claude Haiku 4.5",
235
+ inputPrice: 1,
236
+ outputPrice: 5,
237
+ contextWindow: 2e5,
238
+ maxOutput: 8192
239
+ },
240
+ {
241
+ id: "anthropic/claude-sonnet-4",
242
+ name: "Claude Sonnet 4",
243
+ inputPrice: 3,
244
+ outputPrice: 15,
245
+ contextWindow: 2e5,
246
+ maxOutput: 64e3,
247
+ reasoning: true
248
+ },
249
+ {
250
+ id: "anthropic/claude-opus-4",
251
+ name: "Claude Opus 4",
252
+ inputPrice: 15,
253
+ outputPrice: 75,
254
+ contextWindow: 2e5,
255
+ maxOutput: 32e3,
256
+ reasoning: true
257
+ },
258
+ {
259
+ id: "anthropic/claude-opus-4.5",
260
+ name: "Claude Opus 4.5",
261
+ inputPrice: 5,
262
+ outputPrice: 25,
263
+ contextWindow: 2e5,
264
+ maxOutput: 32e3,
265
+ reasoning: true
266
+ },
121
267
  // Google
122
- { id: "google/gemini-3-pro-preview", name: "Gemini 3 Pro Preview", inputPrice: 2, outputPrice: 12, contextWindow: 105e4, maxOutput: 65536, reasoning: true, vision: true },
123
- { id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro", inputPrice: 1.25, outputPrice: 10, contextWindow: 105e4, maxOutput: 65536, reasoning: true, vision: true },
124
- { id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash", inputPrice: 0.15, outputPrice: 0.6, contextWindow: 1e6, maxOutput: 65536 },
268
+ {
269
+ id: "google/gemini-3-pro-preview",
270
+ name: "Gemini 3 Pro Preview",
271
+ inputPrice: 2,
272
+ outputPrice: 12,
273
+ contextWindow: 105e4,
274
+ maxOutput: 65536,
275
+ reasoning: true,
276
+ vision: true
277
+ },
278
+ {
279
+ id: "google/gemini-2.5-pro",
280
+ name: "Gemini 2.5 Pro",
281
+ inputPrice: 1.25,
282
+ outputPrice: 10,
283
+ contextWindow: 105e4,
284
+ maxOutput: 65536,
285
+ reasoning: true,
286
+ vision: true
287
+ },
288
+ {
289
+ id: "google/gemini-2.5-flash",
290
+ name: "Gemini 2.5 Flash",
291
+ inputPrice: 0.15,
292
+ outputPrice: 0.6,
293
+ contextWindow: 1e6,
294
+ maxOutput: 65536
295
+ },
125
296
  // DeepSeek
126
- { id: "deepseek/deepseek-chat", name: "DeepSeek V3.2 Chat", inputPrice: 0.28, outputPrice: 0.42, contextWindow: 128e3, maxOutput: 8192 },
127
- { id: "deepseek/deepseek-reasoner", name: "DeepSeek V3.2 Reasoner", inputPrice: 0.28, outputPrice: 0.42, contextWindow: 128e3, maxOutput: 8192, reasoning: true },
297
+ {
298
+ id: "deepseek/deepseek-chat",
299
+ name: "DeepSeek V3.2 Chat",
300
+ inputPrice: 0.28,
301
+ outputPrice: 0.42,
302
+ contextWindow: 128e3,
303
+ maxOutput: 8192
304
+ },
305
+ {
306
+ id: "deepseek/deepseek-reasoner",
307
+ name: "DeepSeek V3.2 Reasoner",
308
+ inputPrice: 0.28,
309
+ outputPrice: 0.42,
310
+ contextWindow: 128e3,
311
+ maxOutput: 8192,
312
+ reasoning: true
313
+ },
314
+ // Moonshot / Kimi
315
+ {
316
+ id: "moonshot/kimi-k2.5",
317
+ name: "Kimi K2.5",
318
+ inputPrice: 0.5,
319
+ outputPrice: 2.4,
320
+ contextWindow: 262144,
321
+ maxOutput: 8192,
322
+ reasoning: true,
323
+ vision: true
324
+ },
128
325
  // xAI / Grok
129
- { id: "xai/grok-3", name: "Grok 3", inputPrice: 3, outputPrice: 15, contextWindow: 131072, maxOutput: 16384, reasoning: true },
130
- { id: "xai/grok-3-fast", name: "Grok 3 Fast", inputPrice: 5, outputPrice: 25, contextWindow: 131072, maxOutput: 16384, reasoning: true },
131
- { id: "xai/grok-3-mini", name: "Grok 3 Mini", inputPrice: 0.3, outputPrice: 0.5, contextWindow: 131072, maxOutput: 16384 }
326
+ {
327
+ id: "xai/grok-3",
328
+ name: "Grok 3",
329
+ inputPrice: 3,
330
+ outputPrice: 15,
331
+ contextWindow: 131072,
332
+ maxOutput: 16384,
333
+ reasoning: true
334
+ },
335
+ {
336
+ id: "xai/grok-3-fast",
337
+ name: "Grok 3 Fast",
338
+ inputPrice: 5,
339
+ outputPrice: 25,
340
+ contextWindow: 131072,
341
+ maxOutput: 16384,
342
+ reasoning: true
343
+ },
344
+ {
345
+ id: "xai/grok-3-mini",
346
+ name: "Grok 3 Mini",
347
+ inputPrice: 0.3,
348
+ outputPrice: 0.5,
349
+ contextWindow: 131072,
350
+ maxOutput: 16384
351
+ }
132
352
  ];
133
353
  function toOpenClawModel(m) {
134
354
  return {
@@ -324,7 +544,16 @@ function createPaymentFetch(privateKey) {
324
544
  if (paymentHeader2) {
325
545
  return handle402(input, init, url, endpointPath, paymentHeader2);
326
546
  }
327
- return response2;
547
+ paymentCache.invalidate(endpointPath);
548
+ const cleanResponse = await fetch(input, init);
549
+ if (cleanResponse.status !== 402) {
550
+ return cleanResponse;
551
+ }
552
+ const cleanHeader = cleanResponse.headers.get("x-payment-required");
553
+ if (!cleanHeader) {
554
+ throw new Error("402 response missing x-payment-required header");
555
+ }
556
+ return handle402(input, init, url, endpointPath, cleanHeader);
328
557
  }
329
558
  const response = await fetch(input, init);
330
559
  if (response.status !== 402) {
@@ -384,10 +613,18 @@ function scoreTokenCount(estimatedTokens, thresholds) {
384
613
  function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
385
614
  const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
386
615
  if (matches.length >= thresholds.high) {
387
- return { name, score: scores.high, signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})` };
616
+ return {
617
+ name,
618
+ score: scores.high,
619
+ signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
620
+ };
388
621
  }
389
622
  if (matches.length >= thresholds.low) {
390
- return { name, score: scores.low, signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})` };
623
+ return {
624
+ name,
625
+ score: scores.low,
626
+ signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
627
+ };
391
628
  }
392
629
  return { name, score: scores.none, signal: null };
393
630
  }
@@ -510,9 +747,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
510
747
  const w = weights[d.name] ?? 0;
511
748
  weightedScore += d.score * w;
512
749
  }
513
- const reasoningMatches = config.reasoningKeywords.filter(
514
- (kw) => text.includes(kw.toLowerCase())
515
- );
750
+ const reasoningMatches = config.reasoningKeywords.filter((kw) => text.includes(kw.toLowerCase()));
516
751
  if (reasoningMatches.length >= 2) {
517
752
  const confidence2 = calibrateConfidence(
518
753
  Math.max(weightedScore, 0.3),
@@ -534,10 +769,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
534
769
  distanceFromBoundary = simpleMedium - weightedScore;
535
770
  } else if (weightedScore < mediumComplex) {
536
771
  tier = "MEDIUM";
537
- distanceFromBoundary = Math.min(
538
- weightedScore - simpleMedium,
539
- mediumComplex - weightedScore
540
- );
772
+ distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
541
773
  } else if (weightedScore < complexReasoning) {
542
774
  tier = "COMPLEX";
543
775
  distanceFromBoundary = Math.min(
@@ -642,15 +874,7 @@ var DEFAULT_ROUTING_CONFIG = {
642
874
  "database",
643
875
  "infrastructure"
644
876
  ],
645
- creativeKeywords: [
646
- "story",
647
- "poem",
648
- "compose",
649
- "brainstorm",
650
- "creative",
651
- "imagine",
652
- "write a"
653
- ],
877
+ creativeKeywords: ["story", "poem", "compose", "brainstorm", "creative", "imagine", "write a"],
654
878
  // New dimension keyword lists
655
879
  imperativeVerbs: [
656
880
  "build",
@@ -793,15 +1017,10 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
793
1017
  );
794
1018
  }
795
1019
  const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
796
- const ruleResult = classifyByRules(
797
- prompt,
798
- systemPrompt,
799
- estimatedTokens,
800
- config.scoring
801
- );
1020
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
802
1021
  let tier;
803
1022
  let confidence;
804
- let method = "rules";
1023
+ const method = "rules";
805
1024
  let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
806
1025
  if (ruleResult.tier !== null) {
807
1026
  tier = ruleResult.tier;
@@ -882,14 +1101,16 @@ var RequestDeduplicator = class {
882
1101
  const entry = this.inflight.get(key);
883
1102
  if (!entry) return void 0;
884
1103
  const promise = new Promise((resolve) => {
885
- entry.waiters.push(new Promise((r) => {
886
- const orig = entry.resolve;
887
- entry.resolve = (result) => {
888
- orig(result);
889
- resolve(result);
890
- r(result);
891
- };
892
- }));
1104
+ entry.waiters.push(
1105
+ new Promise((r) => {
1106
+ const orig = entry.resolve;
1107
+ entry.resolve = (result) => {
1108
+ orig(result);
1109
+ resolve(result);
1110
+ r(result);
1111
+ };
1112
+ })
1113
+ );
893
1114
  });
894
1115
  return promise;
895
1116
  }
@@ -928,11 +1149,182 @@ var RequestDeduplicator = class {
928
1149
  }
929
1150
  };
930
1151
 
1152
+ // src/balance.ts
1153
+ import { createPublicClient, http, erc20Abi } from "viem";
1154
+ import { base } from "viem/chains";
1155
+
1156
+ // src/errors.ts
1157
+ var InsufficientFundsError = class extends Error {
1158
+ code = "INSUFFICIENT_FUNDS";
1159
+ currentBalanceUSD;
1160
+ requiredUSD;
1161
+ walletAddress;
1162
+ constructor(opts) {
1163
+ super(
1164
+ `Insufficient USDC balance. Current: ${opts.currentBalanceUSD}, Required: ${opts.requiredUSD}. Fund wallet: ${opts.walletAddress}`
1165
+ );
1166
+ this.name = "InsufficientFundsError";
1167
+ this.currentBalanceUSD = opts.currentBalanceUSD;
1168
+ this.requiredUSD = opts.requiredUSD;
1169
+ this.walletAddress = opts.walletAddress;
1170
+ }
1171
+ };
1172
+ var EmptyWalletError = class extends Error {
1173
+ code = "EMPTY_WALLET";
1174
+ walletAddress;
1175
+ constructor(walletAddress) {
1176
+ super(`No USDC balance. Fund wallet to use ClawRouter: ${walletAddress}`);
1177
+ this.name = "EmptyWalletError";
1178
+ this.walletAddress = walletAddress;
1179
+ }
1180
+ };
1181
+ function isInsufficientFundsError(error) {
1182
+ return error instanceof Error && error.code === "INSUFFICIENT_FUNDS";
1183
+ }
1184
+ function isEmptyWalletError(error) {
1185
+ return error instanceof Error && error.code === "EMPTY_WALLET";
1186
+ }
1187
+ function isBalanceError(error) {
1188
+ return isInsufficientFundsError(error) || isEmptyWalletError(error);
1189
+ }
1190
+ var RpcError = class extends Error {
1191
+ code = "RPC_ERROR";
1192
+ originalError;
1193
+ constructor(message, originalError) {
1194
+ super(`RPC error: ${message}. Check network connectivity.`);
1195
+ this.name = "RpcError";
1196
+ this.originalError = originalError;
1197
+ }
1198
+ };
1199
+ function isRpcError(error) {
1200
+ return error instanceof Error && error.code === "RPC_ERROR";
1201
+ }
1202
+
1203
+ // src/balance.ts
1204
+ var USDC_BASE2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1205
+ var CACHE_TTL_MS = 3e4;
1206
+ var BALANCE_THRESHOLDS = {
1207
+ /** Low balance warning threshold: $1.00 */
1208
+ LOW_BALANCE_MICROS: 1000000n,
1209
+ /** Effectively zero threshold: $0.0001 (covers dust/rounding) */
1210
+ ZERO_THRESHOLD: 100n
1211
+ };
1212
+ var BalanceMonitor = class {
1213
+ client;
1214
+ walletAddress;
1215
+ /** Cached balance (null = not yet fetched) */
1216
+ cachedBalance = null;
1217
+ /** Timestamp when cache was last updated */
1218
+ cachedAt = 0;
1219
+ constructor(walletAddress) {
1220
+ this.walletAddress = walletAddress;
1221
+ this.client = createPublicClient({
1222
+ chain: base,
1223
+ transport: http()
1224
+ });
1225
+ }
1226
+ /**
1227
+ * Check current USDC balance.
1228
+ * Uses cache if valid, otherwise fetches from RPC.
1229
+ */
1230
+ async checkBalance() {
1231
+ const now = Date.now();
1232
+ if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS) {
1233
+ return this.buildInfo(this.cachedBalance);
1234
+ }
1235
+ const balance = await this.fetchBalance();
1236
+ this.cachedBalance = balance;
1237
+ this.cachedAt = now;
1238
+ return this.buildInfo(balance);
1239
+ }
1240
+ /**
1241
+ * Check if balance is sufficient for an estimated cost.
1242
+ *
1243
+ * @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
1244
+ */
1245
+ async checkSufficient(estimatedCostMicros) {
1246
+ const info = await this.checkBalance();
1247
+ if (info.balance >= estimatedCostMicros) {
1248
+ return { sufficient: true, info };
1249
+ }
1250
+ const shortfall = estimatedCostMicros - info.balance;
1251
+ return {
1252
+ sufficient: false,
1253
+ info,
1254
+ shortfall: this.formatUSDC(shortfall)
1255
+ };
1256
+ }
1257
+ /**
1258
+ * Optimistically deduct estimated cost from cached balance.
1259
+ * Call this after a successful payment to keep cache accurate.
1260
+ *
1261
+ * @param amountMicros - Amount to deduct in USDC smallest unit
1262
+ */
1263
+ deductEstimated(amountMicros) {
1264
+ if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
1265
+ this.cachedBalance -= amountMicros;
1266
+ }
1267
+ }
1268
+ /**
1269
+ * Invalidate cache, forcing next checkBalance() to fetch from RPC.
1270
+ * Call this after a payment failure to get accurate balance.
1271
+ */
1272
+ invalidate() {
1273
+ this.cachedBalance = null;
1274
+ this.cachedAt = 0;
1275
+ }
1276
+ /**
1277
+ * Force refresh balance from RPC (ignores cache).
1278
+ */
1279
+ async refresh() {
1280
+ this.invalidate();
1281
+ return this.checkBalance();
1282
+ }
1283
+ /**
1284
+ * Format USDC amount (in micros) as "$X.XX".
1285
+ */
1286
+ formatUSDC(amountMicros) {
1287
+ const dollars = Number(amountMicros) / 1e6;
1288
+ return `$${dollars.toFixed(2)}`;
1289
+ }
1290
+ /**
1291
+ * Get the wallet address being monitored.
1292
+ */
1293
+ getWalletAddress() {
1294
+ return this.walletAddress;
1295
+ }
1296
+ /** Fetch balance from RPC */
1297
+ async fetchBalance() {
1298
+ try {
1299
+ const balance = await this.client.readContract({
1300
+ address: USDC_BASE2,
1301
+ abi: erc20Abi,
1302
+ functionName: "balanceOf",
1303
+ args: [this.walletAddress]
1304
+ });
1305
+ return balance;
1306
+ } catch (error) {
1307
+ throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
1308
+ }
1309
+ }
1310
+ /** Build BalanceInfo from raw balance */
1311
+ buildInfo(balance) {
1312
+ return {
1313
+ balance,
1314
+ balanceUSD: this.formatUSDC(balance),
1315
+ isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
1316
+ isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
1317
+ walletAddress: this.walletAddress
1318
+ };
1319
+ }
1320
+ };
1321
+
931
1322
  // src/proxy.ts
932
1323
  var BLOCKRUN_API = "https://blockrun.ai/api";
933
1324
  var AUTO_MODEL = "blockrun/auto";
934
- var USER_AGENT = "clawrouter/0.3.1";
1325
+ var USER_AGENT = "clawrouter/0.3.2";
935
1326
  var HEARTBEAT_INTERVAL_MS = 2e3;
1327
+ var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
936
1328
  function buildModelPricing() {
937
1329
  const map = /* @__PURE__ */ new Map();
938
1330
  for (const m of BLOCKRUN_MODELS) {
@@ -958,13 +1350,14 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
958
1350
  const estimatedInputTokens = Math.ceil(bodyLength / 4);
959
1351
  const estimatedOutputTokens = maxTokens || model.maxOutput || 4096;
960
1352
  const costUsd = estimatedInputTokens / 1e6 * model.inputPrice + estimatedOutputTokens / 1e6 * model.outputPrice;
961
- const amountMicros = Math.ceil(costUsd * 1.2 * 1e6);
1353
+ const amountMicros = Math.max(100, Math.ceil(costUsd * 1.2 * 1e6));
962
1354
  return amountMicros.toString();
963
1355
  }
964
1356
  async function startProxy(options) {
965
1357
  const apiBase = options.apiBase ?? BLOCKRUN_API;
966
1358
  const account = privateKeyToAccount3(options.walletKey);
967
- const { fetch: payFetch, cache: paymentCache } = createPaymentFetch(options.walletKey);
1359
+ const { fetch: payFetch } = createPaymentFetch(options.walletKey);
1360
+ const balanceMonitor = new BalanceMonitor(account.address);
968
1361
  const routingConfig = mergeRoutingConfig(options.routingConfig);
969
1362
  const modelPricing = buildModelPricing();
970
1363
  const routerOpts = {
@@ -973,9 +1366,25 @@ async function startProxy(options) {
973
1366
  };
974
1367
  const deduplicator = new RequestDeduplicator();
975
1368
  const server = createServer(async (req, res) => {
976
- if (req.url === "/health") {
1369
+ if (req.url === "/health" || req.url?.startsWith("/health?")) {
1370
+ const url = new URL(req.url, "http://localhost");
1371
+ const full = url.searchParams.get("full") === "true";
1372
+ const response = {
1373
+ status: "ok",
1374
+ wallet: account.address
1375
+ };
1376
+ if (full) {
1377
+ try {
1378
+ const balanceInfo = await balanceMonitor.checkBalance();
1379
+ response.balance = balanceInfo.balanceUSD;
1380
+ response.isLow = balanceInfo.isLow;
1381
+ response.isEmpty = balanceInfo.isEmpty;
1382
+ } catch {
1383
+ response.balanceError = "Could not fetch balance";
1384
+ }
1385
+ }
977
1386
  res.writeHead(200, { "Content-Type": "application/json" });
978
- res.end(JSON.stringify({ status: "ok", wallet: account.address }));
1387
+ res.end(JSON.stringify(response));
979
1388
  return;
980
1389
  }
981
1390
  if (!req.url?.startsWith("/v1")) {
@@ -984,19 +1393,32 @@ async function startProxy(options) {
984
1393
  return;
985
1394
  }
986
1395
  try {
987
- await proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator);
1396
+ await proxyRequest(
1397
+ req,
1398
+ res,
1399
+ apiBase,
1400
+ payFetch,
1401
+ options,
1402
+ routerOpts,
1403
+ deduplicator,
1404
+ balanceMonitor
1405
+ );
988
1406
  } catch (err) {
989
1407
  const error = err instanceof Error ? err : new Error(String(err));
990
1408
  options.onError?.(error);
991
1409
  if (!res.headersSent) {
992
1410
  res.writeHead(502, { "Content-Type": "application/json" });
993
- res.end(JSON.stringify({
994
- error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
995
- }));
1411
+ res.end(
1412
+ JSON.stringify({
1413
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
1414
+ })
1415
+ );
996
1416
  } else if (!res.writableEnded) {
997
- res.write(`data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
1417
+ res.write(
1418
+ `data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
998
1419
 
999
- `);
1420
+ `
1421
+ );
1000
1422
  res.write("data: [DONE]\n\n");
1001
1423
  res.end();
1002
1424
  }
@@ -1013,6 +1435,8 @@ async function startProxy(options) {
1013
1435
  resolve({
1014
1436
  port,
1015
1437
  baseUrl,
1438
+ walletAddress: account.address,
1439
+ balanceMonitor,
1016
1440
  close: () => new Promise((res, rej) => {
1017
1441
  server.close((err) => err ? rej(err) : res());
1018
1442
  })
@@ -1020,7 +1444,7 @@ async function startProxy(options) {
1020
1444
  });
1021
1445
  });
1022
1446
  }
1023
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator) {
1447
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
1024
1448
  const startTime = Date.now();
1025
1449
  const upstreamUrl = `${apiBase}${req.url}`;
1026
1450
  const bodyChunks = [];
@@ -1077,13 +1501,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1077
1501
  return;
1078
1502
  }
1079
1503
  deduplicator.markInflight(dedupKey);
1504
+ let estimatedCostMicros;
1505
+ if (modelId) {
1506
+ const estimated = estimateAmount(modelId, body.length, maxTokens);
1507
+ if (estimated) {
1508
+ estimatedCostMicros = BigInt(estimated);
1509
+ const sufficiency = await balanceMonitor.checkSufficient(estimatedCostMicros);
1510
+ if (sufficiency.info.isEmpty) {
1511
+ deduplicator.removeInflight(dedupKey);
1512
+ const error = new EmptyWalletError(sufficiency.info.walletAddress);
1513
+ options.onInsufficientFunds?.({
1514
+ balanceUSD: sufficiency.info.balanceUSD,
1515
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1516
+ walletAddress: sufficiency.info.walletAddress
1517
+ });
1518
+ throw error;
1519
+ }
1520
+ if (!sufficiency.sufficient) {
1521
+ deduplicator.removeInflight(dedupKey);
1522
+ const error = new InsufficientFundsError({
1523
+ currentBalanceUSD: sufficiency.info.balanceUSD,
1524
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1525
+ walletAddress: sufficiency.info.walletAddress
1526
+ });
1527
+ options.onInsufficientFunds?.({
1528
+ balanceUSD: sufficiency.info.balanceUSD,
1529
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1530
+ walletAddress: sufficiency.info.walletAddress
1531
+ });
1532
+ throw error;
1533
+ }
1534
+ if (sufficiency.info.isLow) {
1535
+ options.onLowBalance?.({
1536
+ balanceUSD: sufficiency.info.balanceUSD,
1537
+ walletAddress: sufficiency.info.walletAddress
1538
+ });
1539
+ }
1540
+ }
1541
+ }
1080
1542
  let heartbeatInterval;
1081
1543
  let headersSentEarly = false;
1082
1544
  if (isStreaming) {
1083
1545
  res.writeHead(200, {
1084
1546
  "content-type": "text/event-stream",
1085
1547
  "cache-control": "no-cache",
1086
- "connection": "keep-alive"
1548
+ connection: "keep-alive"
1087
1549
  });
1088
1550
  headersSentEarly = true;
1089
1551
  res.write(": heartbeat\n\n");
@@ -1095,7 +1557,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1095
1557
  }
1096
1558
  const headers = {};
1097
1559
  for (const [key, value] of Object.entries(req.headers)) {
1098
- if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length") continue;
1560
+ if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length")
1561
+ continue;
1099
1562
  if (typeof value === "string") {
1100
1563
  headers[key] = value;
1101
1564
  }
@@ -1105,18 +1568,34 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1105
1568
  }
1106
1569
  headers["user-agent"] = USER_AGENT;
1107
1570
  let preAuth;
1108
- if (modelId) {
1109
- const estimated = estimateAmount(modelId, body.length, maxTokens);
1110
- if (estimated) {
1111
- preAuth = { estimatedAmount: estimated };
1112
- }
1571
+ if (estimatedCostMicros !== void 0) {
1572
+ preAuth = { estimatedAmount: estimatedCostMicros.toString() };
1113
1573
  }
1574
+ let completed = false;
1575
+ res.on("close", () => {
1576
+ if (heartbeatInterval) {
1577
+ clearInterval(heartbeatInterval);
1578
+ heartbeatInterval = void 0;
1579
+ }
1580
+ if (!completed) {
1581
+ deduplicator.removeInflight(dedupKey);
1582
+ }
1583
+ });
1584
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1585
+ const controller = new AbortController();
1586
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1114
1587
  try {
1115
- const upstream = await payFetch(upstreamUrl, {
1116
- method: req.method ?? "POST",
1117
- headers,
1118
- body: body.length > 0 ? body : void 0
1119
- }, preAuth);
1588
+ const upstream = await payFetch(
1589
+ upstreamUrl,
1590
+ {
1591
+ method: req.method ?? "POST",
1592
+ headers,
1593
+ body: body.length > 0 ? body : void 0,
1594
+ signal: controller.signal
1595
+ },
1596
+ preAuth
1597
+ );
1598
+ clearTimeout(timeoutId);
1120
1599
  if (heartbeatInterval) {
1121
1600
  clearInterval(heartbeatInterval);
1122
1601
  heartbeatInterval = void 0;
@@ -1189,11 +1668,21 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1189
1668
  completedAt: Date.now()
1190
1669
  });
1191
1670
  }
1671
+ if (estimatedCostMicros !== void 0) {
1672
+ balanceMonitor.deductEstimated(estimatedCostMicros);
1673
+ }
1674
+ completed = true;
1192
1675
  } catch (err) {
1676
+ clearTimeout(timeoutId);
1193
1677
  if (heartbeatInterval) {
1194
1678
  clearInterval(heartbeatInterval);
1679
+ heartbeatInterval = void 0;
1195
1680
  }
1196
1681
  deduplicator.removeInflight(dedupKey);
1682
+ balanceMonitor.invalidate();
1683
+ if (err instanceof Error && err.name === "AbortError") {
1684
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
1685
+ }
1197
1686
  throw err;
1198
1687
  }
1199
1688
  if (routingDecision) {
@@ -1208,6 +1697,62 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1208
1697
  }
1209
1698
  }
1210
1699
 
1700
+ // src/retry.ts
1701
+ var DEFAULT_RETRY_CONFIG = {
1702
+ maxRetries: 2,
1703
+ baseDelayMs: 500,
1704
+ retryableCodes: [429, 502, 503, 504]
1705
+ };
1706
+ function sleep(ms) {
1707
+ return new Promise((resolve) => setTimeout(resolve, ms));
1708
+ }
1709
+ async function fetchWithRetry(fetchFn, url, init, config) {
1710
+ const cfg = {
1711
+ ...DEFAULT_RETRY_CONFIG,
1712
+ ...config
1713
+ };
1714
+ let lastError;
1715
+ let lastResponse;
1716
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
1717
+ try {
1718
+ const response = await fetchFn(url, init);
1719
+ if (!cfg.retryableCodes.includes(response.status)) {
1720
+ return response;
1721
+ }
1722
+ lastResponse = response;
1723
+ const retryAfter = response.headers.get("retry-after");
1724
+ let delay;
1725
+ if (retryAfter) {
1726
+ const seconds = parseInt(retryAfter, 10);
1727
+ delay = isNaN(seconds) ? cfg.baseDelayMs * Math.pow(2, attempt) : seconds * 1e3;
1728
+ } else {
1729
+ delay = cfg.baseDelayMs * Math.pow(2, attempt);
1730
+ }
1731
+ if (attempt < cfg.maxRetries) {
1732
+ await sleep(delay);
1733
+ }
1734
+ } catch (err) {
1735
+ lastError = err instanceof Error ? err : new Error(String(err));
1736
+ if (attempt < cfg.maxRetries) {
1737
+ const delay = cfg.baseDelayMs * Math.pow(2, attempt);
1738
+ await sleep(delay);
1739
+ }
1740
+ }
1741
+ }
1742
+ if (lastResponse) {
1743
+ return lastResponse;
1744
+ }
1745
+ throw lastError ?? new Error("Max retries exceeded");
1746
+ }
1747
+ function isRetryable(errorOrResponse, config) {
1748
+ const retryableCodes = config?.retryableCodes ?? DEFAULT_RETRY_CONFIG.retryableCodes;
1749
+ if (errorOrResponse instanceof Response) {
1750
+ return retryableCodes.includes(errorOrResponse.status);
1751
+ }
1752
+ const message = errorOrResponse.message.toLowerCase();
1753
+ return message.includes("network") || message.includes("timeout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("socket hang up");
1754
+ }
1755
+
1211
1756
  // src/index.ts
1212
1757
  async function startProxyInBackground(api) {
1213
1758
  const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
@@ -1219,6 +1764,23 @@ async function startProxyInBackground(api) {
1219
1764
  } else {
1220
1765
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
1221
1766
  }
1767
+ const startupMonitor = new BalanceMonitor(address);
1768
+ try {
1769
+ const startupBalance = await startupMonitor.checkBalance();
1770
+ if (startupBalance.isEmpty) {
1771
+ api.logger.warn(`[!] No USDC balance. Fund wallet to use ClawRouter: ${address}`);
1772
+ } else if (startupBalance.isLow) {
1773
+ api.logger.warn(
1774
+ `[!] Low balance: ${startupBalance.balanceUSD} remaining. Fund wallet: ${address}`
1775
+ );
1776
+ } else {
1777
+ api.logger.info(`Wallet balance: ${startupBalance.balanceUSD}`);
1778
+ }
1779
+ } catch (err) {
1780
+ api.logger.warn(
1781
+ `Could not check wallet balance: ${err instanceof Error ? err.message : String(err)}`
1782
+ );
1783
+ }
1222
1784
  const routingConfig = api.pluginConfig?.routing;
1223
1785
  const proxy = await startProxy({
1224
1786
  walletKey,
@@ -1233,6 +1795,14 @@ async function startProxyInBackground(api) {
1233
1795
  const cost = decision.costEstimate.toFixed(4);
1234
1796
  const saved = (decision.savings * 100).toFixed(0);
1235
1797
  api.logger.info(`${decision.model} $${cost} (saved ${saved}%)`);
1798
+ },
1799
+ onLowBalance: (info) => {
1800
+ api.logger.warn(`[!] Low balance: ${info.balanceUSD}. Fund wallet: ${info.walletAddress}`);
1801
+ },
1802
+ onInsufficientFunds: (info) => {
1803
+ api.logger.error(
1804
+ `[!] Insufficient funds. Balance: ${info.balanceUSD}, Needed: ${info.requiredUSD}. Fund wallet: ${info.walletAddress}`
1805
+ );
1236
1806
  }
1237
1807
  });
1238
1808
  setActiveProxy(proxy);
@@ -1242,7 +1812,7 @@ var plugin = {
1242
1812
  id: "clawrouter",
1243
1813
  name: "ClawRouter",
1244
1814
  description: "Smart LLM router \u2014 30+ models, x402 micropayments, 78% cost savings",
1245
- version: "0.3.1",
1815
+ version: "0.3.2",
1246
1816
  register(api) {
1247
1817
  api.registerProvider(blockrunProvider);
1248
1818
  api.logger.info("BlockRun provider registered (30+ models via x402)");
@@ -1255,15 +1825,27 @@ var plugin = {
1255
1825
  };
1256
1826
  var index_default = plugin;
1257
1827
  export {
1828
+ BALANCE_THRESHOLDS,
1258
1829
  BLOCKRUN_MODELS,
1830
+ BalanceMonitor,
1831
+ DEFAULT_RETRY_CONFIG,
1259
1832
  DEFAULT_ROUTING_CONFIG,
1833
+ EmptyWalletError,
1834
+ InsufficientFundsError,
1260
1835
  OPENCLAW_MODELS,
1261
1836
  PaymentCache,
1262
1837
  RequestDeduplicator,
1838
+ RpcError,
1263
1839
  blockrunProvider,
1264
1840
  buildProviderModels,
1265
1841
  createPaymentFetch,
1266
1842
  index_default as default,
1843
+ fetchWithRetry,
1844
+ isBalanceError,
1845
+ isEmptyWalletError,
1846
+ isInsufficientFundsError,
1847
+ isRetryable,
1848
+ isRpcError,
1267
1849
  logUsage,
1268
1850
  route,
1269
1851
  startProxy