@blockrun/clawrouter 0.3.2 → 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 {
@@ -393,10 +613,18 @@ function scoreTokenCount(estimatedTokens, thresholds) {
393
613
  function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
394
614
  const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
395
615
  if (matches.length >= thresholds.high) {
396
- 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
+ };
397
621
  }
398
622
  if (matches.length >= thresholds.low) {
399
- 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
+ };
400
628
  }
401
629
  return { name, score: scores.none, signal: null };
402
630
  }
@@ -519,9 +747,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
519
747
  const w = weights[d.name] ?? 0;
520
748
  weightedScore += d.score * w;
521
749
  }
522
- const reasoningMatches = config.reasoningKeywords.filter(
523
- (kw) => text.includes(kw.toLowerCase())
524
- );
750
+ const reasoningMatches = config.reasoningKeywords.filter((kw) => text.includes(kw.toLowerCase()));
525
751
  if (reasoningMatches.length >= 2) {
526
752
  const confidence2 = calibrateConfidence(
527
753
  Math.max(weightedScore, 0.3),
@@ -543,10 +769,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
543
769
  distanceFromBoundary = simpleMedium - weightedScore;
544
770
  } else if (weightedScore < mediumComplex) {
545
771
  tier = "MEDIUM";
546
- distanceFromBoundary = Math.min(
547
- weightedScore - simpleMedium,
548
- mediumComplex - weightedScore
549
- );
772
+ distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
550
773
  } else if (weightedScore < complexReasoning) {
551
774
  tier = "COMPLEX";
552
775
  distanceFromBoundary = Math.min(
@@ -651,15 +874,7 @@ var DEFAULT_ROUTING_CONFIG = {
651
874
  "database",
652
875
  "infrastructure"
653
876
  ],
654
- creativeKeywords: [
655
- "story",
656
- "poem",
657
- "compose",
658
- "brainstorm",
659
- "creative",
660
- "imagine",
661
- "write a"
662
- ],
877
+ creativeKeywords: ["story", "poem", "compose", "brainstorm", "creative", "imagine", "write a"],
663
878
  // New dimension keyword lists
664
879
  imperativeVerbs: [
665
880
  "build",
@@ -802,15 +1017,10 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
802
1017
  );
803
1018
  }
804
1019
  const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
805
- const ruleResult = classifyByRules(
806
- prompt,
807
- systemPrompt,
808
- estimatedTokens,
809
- config.scoring
810
- );
1020
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
811
1021
  let tier;
812
1022
  let confidence;
813
- let method = "rules";
1023
+ const method = "rules";
814
1024
  let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
815
1025
  if (ruleResult.tier !== null) {
816
1026
  tier = ruleResult.tier;
@@ -891,14 +1101,16 @@ var RequestDeduplicator = class {
891
1101
  const entry = this.inflight.get(key);
892
1102
  if (!entry) return void 0;
893
1103
  const promise = new Promise((resolve) => {
894
- entry.waiters.push(new Promise((r) => {
895
- const orig = entry.resolve;
896
- entry.resolve = (result) => {
897
- orig(result);
898
- resolve(result);
899
- r(result);
900
- };
901
- }));
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
+ );
902
1114
  });
903
1115
  return promise;
904
1116
  }
@@ -937,11 +1149,182 @@ var RequestDeduplicator = class {
937
1149
  }
938
1150
  };
939
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
+
940
1322
  // src/proxy.ts
941
1323
  var BLOCKRUN_API = "https://blockrun.ai/api";
942
1324
  var AUTO_MODEL = "blockrun/auto";
943
1325
  var USER_AGENT = "clawrouter/0.3.2";
944
1326
  var HEARTBEAT_INTERVAL_MS = 2e3;
1327
+ var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
945
1328
  function buildModelPricing() {
946
1329
  const map = /* @__PURE__ */ new Map();
947
1330
  for (const m of BLOCKRUN_MODELS) {
@@ -973,7 +1356,8 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
973
1356
  async function startProxy(options) {
974
1357
  const apiBase = options.apiBase ?? BLOCKRUN_API;
975
1358
  const account = privateKeyToAccount3(options.walletKey);
976
- const { fetch: payFetch, cache: paymentCache } = createPaymentFetch(options.walletKey);
1359
+ const { fetch: payFetch } = createPaymentFetch(options.walletKey);
1360
+ const balanceMonitor = new BalanceMonitor(account.address);
977
1361
  const routingConfig = mergeRoutingConfig(options.routingConfig);
978
1362
  const modelPricing = buildModelPricing();
979
1363
  const routerOpts = {
@@ -982,9 +1366,25 @@ async function startProxy(options) {
982
1366
  };
983
1367
  const deduplicator = new RequestDeduplicator();
984
1368
  const server = createServer(async (req, res) => {
985
- 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
+ }
986
1386
  res.writeHead(200, { "Content-Type": "application/json" });
987
- res.end(JSON.stringify({ status: "ok", wallet: account.address }));
1387
+ res.end(JSON.stringify(response));
988
1388
  return;
989
1389
  }
990
1390
  if (!req.url?.startsWith("/v1")) {
@@ -993,19 +1393,32 @@ async function startProxy(options) {
993
1393
  return;
994
1394
  }
995
1395
  try {
996
- 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
+ );
997
1406
  } catch (err) {
998
1407
  const error = err instanceof Error ? err : new Error(String(err));
999
1408
  options.onError?.(error);
1000
1409
  if (!res.headersSent) {
1001
1410
  res.writeHead(502, { "Content-Type": "application/json" });
1002
- res.end(JSON.stringify({
1003
- error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
1004
- }));
1411
+ res.end(
1412
+ JSON.stringify({
1413
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
1414
+ })
1415
+ );
1005
1416
  } else if (!res.writableEnded) {
1006
- 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" } })}
1007
1419
 
1008
- `);
1420
+ `
1421
+ );
1009
1422
  res.write("data: [DONE]\n\n");
1010
1423
  res.end();
1011
1424
  }
@@ -1022,6 +1435,8 @@ async function startProxy(options) {
1022
1435
  resolve({
1023
1436
  port,
1024
1437
  baseUrl,
1438
+ walletAddress: account.address,
1439
+ balanceMonitor,
1025
1440
  close: () => new Promise((res, rej) => {
1026
1441
  server.close((err) => err ? rej(err) : res());
1027
1442
  })
@@ -1029,7 +1444,7 @@ async function startProxy(options) {
1029
1444
  });
1030
1445
  });
1031
1446
  }
1032
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator) {
1447
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
1033
1448
  const startTime = Date.now();
1034
1449
  const upstreamUrl = `${apiBase}${req.url}`;
1035
1450
  const bodyChunks = [];
@@ -1086,13 +1501,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1086
1501
  return;
1087
1502
  }
1088
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
+ }
1089
1542
  let heartbeatInterval;
1090
1543
  let headersSentEarly = false;
1091
1544
  if (isStreaming) {
1092
1545
  res.writeHead(200, {
1093
1546
  "content-type": "text/event-stream",
1094
1547
  "cache-control": "no-cache",
1095
- "connection": "keep-alive"
1548
+ connection: "keep-alive"
1096
1549
  });
1097
1550
  headersSentEarly = true;
1098
1551
  res.write(": heartbeat\n\n");
@@ -1104,7 +1557,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1104
1557
  }
1105
1558
  const headers = {};
1106
1559
  for (const [key, value] of Object.entries(req.headers)) {
1107
- 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;
1108
1562
  if (typeof value === "string") {
1109
1563
  headers[key] = value;
1110
1564
  }
@@ -1114,18 +1568,34 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1114
1568
  }
1115
1569
  headers["user-agent"] = USER_AGENT;
1116
1570
  let preAuth;
1117
- if (modelId) {
1118
- const estimated = estimateAmount(modelId, body.length, maxTokens);
1119
- if (estimated) {
1120
- preAuth = { estimatedAmount: estimated };
1121
- }
1571
+ if (estimatedCostMicros !== void 0) {
1572
+ preAuth = { estimatedAmount: estimatedCostMicros.toString() };
1122
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);
1123
1587
  try {
1124
- const upstream = await payFetch(upstreamUrl, {
1125
- method: req.method ?? "POST",
1126
- headers,
1127
- body: body.length > 0 ? body : void 0
1128
- }, 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);
1129
1599
  if (heartbeatInterval) {
1130
1600
  clearInterval(heartbeatInterval);
1131
1601
  heartbeatInterval = void 0;
@@ -1198,11 +1668,21 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1198
1668
  completedAt: Date.now()
1199
1669
  });
1200
1670
  }
1671
+ if (estimatedCostMicros !== void 0) {
1672
+ balanceMonitor.deductEstimated(estimatedCostMicros);
1673
+ }
1674
+ completed = true;
1201
1675
  } catch (err) {
1676
+ clearTimeout(timeoutId);
1202
1677
  if (heartbeatInterval) {
1203
1678
  clearInterval(heartbeatInterval);
1679
+ heartbeatInterval = void 0;
1204
1680
  }
1205
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
+ }
1206
1686
  throw err;
1207
1687
  }
1208
1688
  if (routingDecision) {
@@ -1217,6 +1697,62 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1217
1697
  }
1218
1698
  }
1219
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
+
1220
1756
  // src/index.ts
1221
1757
  async function startProxyInBackground(api) {
1222
1758
  const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
@@ -1228,6 +1764,23 @@ async function startProxyInBackground(api) {
1228
1764
  } else {
1229
1765
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
1230
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
+ }
1231
1784
  const routingConfig = api.pluginConfig?.routing;
1232
1785
  const proxy = await startProxy({
1233
1786
  walletKey,
@@ -1242,6 +1795,14 @@ async function startProxyInBackground(api) {
1242
1795
  const cost = decision.costEstimate.toFixed(4);
1243
1796
  const saved = (decision.savings * 100).toFixed(0);
1244
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
+ );
1245
1806
  }
1246
1807
  });
1247
1808
  setActiveProxy(proxy);
@@ -1264,15 +1825,27 @@ var plugin = {
1264
1825
  };
1265
1826
  var index_default = plugin;
1266
1827
  export {
1828
+ BALANCE_THRESHOLDS,
1267
1829
  BLOCKRUN_MODELS,
1830
+ BalanceMonitor,
1831
+ DEFAULT_RETRY_CONFIG,
1268
1832
  DEFAULT_ROUTING_CONFIG,
1833
+ EmptyWalletError,
1834
+ InsufficientFundsError,
1269
1835
  OPENCLAW_MODELS,
1270
1836
  PaymentCache,
1271
1837
  RequestDeduplicator,
1838
+ RpcError,
1272
1839
  blockrunProvider,
1273
1840
  buildProviderModels,
1274
1841
  createPaymentFetch,
1275
1842
  index_default as default,
1843
+ fetchWithRetry,
1844
+ isBalanceError,
1845
+ isEmptyWalletError,
1846
+ isInsufficientFundsError,
1847
+ isRetryable,
1848
+ isRpcError,
1276
1849
  logUsage,
1277
1850
  route,
1278
1851
  startProxy