@blockrun/clawrouter 0.3.2 → 0.3.4

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,183 @@ 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
- var USER_AGENT = "clawrouter/0.3.2";
1325
+ var USER_AGENT = "clawrouter/0.3.4";
944
1326
  var HEARTBEAT_INTERVAL_MS = 2e3;
1327
+ var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
1328
+ var DEFAULT_PORT = 8402;
945
1329
  function buildModelPricing() {
946
1330
  const map = /* @__PURE__ */ new Map();
947
1331
  for (const m of BLOCKRUN_MODELS) {
@@ -973,7 +1357,8 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
973
1357
  async function startProxy(options) {
974
1358
  const apiBase = options.apiBase ?? BLOCKRUN_API;
975
1359
  const account = privateKeyToAccount3(options.walletKey);
976
- const { fetch: payFetch, cache: paymentCache } = createPaymentFetch(options.walletKey);
1360
+ const { fetch: payFetch } = createPaymentFetch(options.walletKey);
1361
+ const balanceMonitor = new BalanceMonitor(account.address);
977
1362
  const routingConfig = mergeRoutingConfig(options.routingConfig);
978
1363
  const modelPricing = buildModelPricing();
979
1364
  const routerOpts = {
@@ -982,9 +1367,25 @@ async function startProxy(options) {
982
1367
  };
983
1368
  const deduplicator = new RequestDeduplicator();
984
1369
  const server = createServer(async (req, res) => {
985
- if (req.url === "/health") {
1370
+ if (req.url === "/health" || req.url?.startsWith("/health?")) {
1371
+ const url = new URL(req.url, "http://localhost");
1372
+ const full = url.searchParams.get("full") === "true";
1373
+ const response = {
1374
+ status: "ok",
1375
+ wallet: account.address
1376
+ };
1377
+ if (full) {
1378
+ try {
1379
+ const balanceInfo = await balanceMonitor.checkBalance();
1380
+ response.balance = balanceInfo.balanceUSD;
1381
+ response.isLow = balanceInfo.isLow;
1382
+ response.isEmpty = balanceInfo.isEmpty;
1383
+ } catch {
1384
+ response.balanceError = "Could not fetch balance";
1385
+ }
1386
+ }
986
1387
  res.writeHead(200, { "Content-Type": "application/json" });
987
- res.end(JSON.stringify({ status: "ok", wallet: account.address }));
1388
+ res.end(JSON.stringify(response));
988
1389
  return;
989
1390
  }
990
1391
  if (!req.url?.startsWith("/v1")) {
@@ -993,25 +1394,38 @@ async function startProxy(options) {
993
1394
  return;
994
1395
  }
995
1396
  try {
996
- await proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator);
1397
+ await proxyRequest(
1398
+ req,
1399
+ res,
1400
+ apiBase,
1401
+ payFetch,
1402
+ options,
1403
+ routerOpts,
1404
+ deduplicator,
1405
+ balanceMonitor
1406
+ );
997
1407
  } catch (err) {
998
1408
  const error = err instanceof Error ? err : new Error(String(err));
999
1409
  options.onError?.(error);
1000
1410
  if (!res.headersSent) {
1001
1411
  res.writeHead(502, { "Content-Type": "application/json" });
1002
- res.end(JSON.stringify({
1003
- error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
1004
- }));
1412
+ res.end(
1413
+ JSON.stringify({
1414
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
1415
+ })
1416
+ );
1005
1417
  } else if (!res.writableEnded) {
1006
- res.write(`data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
1418
+ res.write(
1419
+ `data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
1007
1420
 
1008
- `);
1421
+ `
1422
+ );
1009
1423
  res.write("data: [DONE]\n\n");
1010
1424
  res.end();
1011
1425
  }
1012
1426
  }
1013
1427
  });
1014
- const listenPort = options.port ?? 0;
1428
+ const listenPort = options.port ?? DEFAULT_PORT;
1015
1429
  return new Promise((resolve, reject) => {
1016
1430
  server.on("error", reject);
1017
1431
  server.listen(listenPort, "127.0.0.1", () => {
@@ -1022,6 +1436,8 @@ async function startProxy(options) {
1022
1436
  resolve({
1023
1437
  port,
1024
1438
  baseUrl,
1439
+ walletAddress: account.address,
1440
+ balanceMonitor,
1025
1441
  close: () => new Promise((res, rej) => {
1026
1442
  server.close((err) => err ? rej(err) : res());
1027
1443
  })
@@ -1029,7 +1445,7 @@ async function startProxy(options) {
1029
1445
  });
1030
1446
  });
1031
1447
  }
1032
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator) {
1448
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
1033
1449
  const startTime = Date.now();
1034
1450
  const upstreamUrl = `${apiBase}${req.url}`;
1035
1451
  const bodyChunks = [];
@@ -1086,13 +1502,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1086
1502
  return;
1087
1503
  }
1088
1504
  deduplicator.markInflight(dedupKey);
1505
+ let estimatedCostMicros;
1506
+ if (modelId) {
1507
+ const estimated = estimateAmount(modelId, body.length, maxTokens);
1508
+ if (estimated) {
1509
+ estimatedCostMicros = BigInt(estimated);
1510
+ const sufficiency = await balanceMonitor.checkSufficient(estimatedCostMicros);
1511
+ if (sufficiency.info.isEmpty) {
1512
+ deduplicator.removeInflight(dedupKey);
1513
+ const error = new EmptyWalletError(sufficiency.info.walletAddress);
1514
+ options.onInsufficientFunds?.({
1515
+ balanceUSD: sufficiency.info.balanceUSD,
1516
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1517
+ walletAddress: sufficiency.info.walletAddress
1518
+ });
1519
+ throw error;
1520
+ }
1521
+ if (!sufficiency.sufficient) {
1522
+ deduplicator.removeInflight(dedupKey);
1523
+ const error = new InsufficientFundsError({
1524
+ currentBalanceUSD: sufficiency.info.balanceUSD,
1525
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1526
+ walletAddress: sufficiency.info.walletAddress
1527
+ });
1528
+ options.onInsufficientFunds?.({
1529
+ balanceUSD: sufficiency.info.balanceUSD,
1530
+ requiredUSD: balanceMonitor.formatUSDC(estimatedCostMicros),
1531
+ walletAddress: sufficiency.info.walletAddress
1532
+ });
1533
+ throw error;
1534
+ }
1535
+ if (sufficiency.info.isLow) {
1536
+ options.onLowBalance?.({
1537
+ balanceUSD: sufficiency.info.balanceUSD,
1538
+ walletAddress: sufficiency.info.walletAddress
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1089
1543
  let heartbeatInterval;
1090
1544
  let headersSentEarly = false;
1091
1545
  if (isStreaming) {
1092
1546
  res.writeHead(200, {
1093
1547
  "content-type": "text/event-stream",
1094
1548
  "cache-control": "no-cache",
1095
- "connection": "keep-alive"
1549
+ connection: "keep-alive"
1096
1550
  });
1097
1551
  headersSentEarly = true;
1098
1552
  res.write(": heartbeat\n\n");
@@ -1104,7 +1558,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1104
1558
  }
1105
1559
  const headers = {};
1106
1560
  for (const [key, value] of Object.entries(req.headers)) {
1107
- if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length") continue;
1561
+ if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length")
1562
+ continue;
1108
1563
  if (typeof value === "string") {
1109
1564
  headers[key] = value;
1110
1565
  }
@@ -1114,18 +1569,34 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1114
1569
  }
1115
1570
  headers["user-agent"] = USER_AGENT;
1116
1571
  let preAuth;
1117
- if (modelId) {
1118
- const estimated = estimateAmount(modelId, body.length, maxTokens);
1119
- if (estimated) {
1120
- preAuth = { estimatedAmount: estimated };
1121
- }
1572
+ if (estimatedCostMicros !== void 0) {
1573
+ preAuth = { estimatedAmount: estimatedCostMicros.toString() };
1122
1574
  }
1575
+ let completed = false;
1576
+ res.on("close", () => {
1577
+ if (heartbeatInterval) {
1578
+ clearInterval(heartbeatInterval);
1579
+ heartbeatInterval = void 0;
1580
+ }
1581
+ if (!completed) {
1582
+ deduplicator.removeInflight(dedupKey);
1583
+ }
1584
+ });
1585
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1586
+ const controller = new AbortController();
1587
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1123
1588
  try {
1124
- const upstream = await payFetch(upstreamUrl, {
1125
- method: req.method ?? "POST",
1126
- headers,
1127
- body: body.length > 0 ? body : void 0
1128
- }, preAuth);
1589
+ const upstream = await payFetch(
1590
+ upstreamUrl,
1591
+ {
1592
+ method: req.method ?? "POST",
1593
+ headers,
1594
+ body: body.length > 0 ? body : void 0,
1595
+ signal: controller.signal
1596
+ },
1597
+ preAuth
1598
+ );
1599
+ clearTimeout(timeoutId);
1129
1600
  if (heartbeatInterval) {
1130
1601
  clearInterval(heartbeatInterval);
1131
1602
  heartbeatInterval = void 0;
@@ -1198,11 +1669,21 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1198
1669
  completedAt: Date.now()
1199
1670
  });
1200
1671
  }
1672
+ if (estimatedCostMicros !== void 0) {
1673
+ balanceMonitor.deductEstimated(estimatedCostMicros);
1674
+ }
1675
+ completed = true;
1201
1676
  } catch (err) {
1677
+ clearTimeout(timeoutId);
1202
1678
  if (heartbeatInterval) {
1203
1679
  clearInterval(heartbeatInterval);
1680
+ heartbeatInterval = void 0;
1204
1681
  }
1205
1682
  deduplicator.removeInflight(dedupKey);
1683
+ balanceMonitor.invalidate();
1684
+ if (err instanceof Error && err.name === "AbortError") {
1685
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
1686
+ }
1206
1687
  throw err;
1207
1688
  }
1208
1689
  if (routingDecision) {
@@ -1217,6 +1698,62 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1217
1698
  }
1218
1699
  }
1219
1700
 
1701
+ // src/retry.ts
1702
+ var DEFAULT_RETRY_CONFIG = {
1703
+ maxRetries: 2,
1704
+ baseDelayMs: 500,
1705
+ retryableCodes: [429, 502, 503, 504]
1706
+ };
1707
+ function sleep(ms) {
1708
+ return new Promise((resolve) => setTimeout(resolve, ms));
1709
+ }
1710
+ async function fetchWithRetry(fetchFn, url, init, config) {
1711
+ const cfg = {
1712
+ ...DEFAULT_RETRY_CONFIG,
1713
+ ...config
1714
+ };
1715
+ let lastError;
1716
+ let lastResponse;
1717
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
1718
+ try {
1719
+ const response = await fetchFn(url, init);
1720
+ if (!cfg.retryableCodes.includes(response.status)) {
1721
+ return response;
1722
+ }
1723
+ lastResponse = response;
1724
+ const retryAfter = response.headers.get("retry-after");
1725
+ let delay;
1726
+ if (retryAfter) {
1727
+ const seconds = parseInt(retryAfter, 10);
1728
+ delay = isNaN(seconds) ? cfg.baseDelayMs * Math.pow(2, attempt) : seconds * 1e3;
1729
+ } else {
1730
+ delay = cfg.baseDelayMs * Math.pow(2, attempt);
1731
+ }
1732
+ if (attempt < cfg.maxRetries) {
1733
+ await sleep(delay);
1734
+ }
1735
+ } catch (err) {
1736
+ lastError = err instanceof Error ? err : new Error(String(err));
1737
+ if (attempt < cfg.maxRetries) {
1738
+ const delay = cfg.baseDelayMs * Math.pow(2, attempt);
1739
+ await sleep(delay);
1740
+ }
1741
+ }
1742
+ }
1743
+ if (lastResponse) {
1744
+ return lastResponse;
1745
+ }
1746
+ throw lastError ?? new Error("Max retries exceeded");
1747
+ }
1748
+ function isRetryable(errorOrResponse, config) {
1749
+ const retryableCodes = config?.retryableCodes ?? DEFAULT_RETRY_CONFIG.retryableCodes;
1750
+ if (errorOrResponse instanceof Response) {
1751
+ return retryableCodes.includes(errorOrResponse.status);
1752
+ }
1753
+ const message = errorOrResponse.message.toLowerCase();
1754
+ return message.includes("network") || message.includes("timeout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("socket hang up");
1755
+ }
1756
+
1220
1757
  // src/index.ts
1221
1758
  async function startProxyInBackground(api) {
1222
1759
  const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
@@ -1228,6 +1765,23 @@ async function startProxyInBackground(api) {
1228
1765
  } else {
1229
1766
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
1230
1767
  }
1768
+ const startupMonitor = new BalanceMonitor(address);
1769
+ try {
1770
+ const startupBalance = await startupMonitor.checkBalance();
1771
+ if (startupBalance.isEmpty) {
1772
+ api.logger.warn(`[!] No USDC balance. Fund wallet to use ClawRouter: ${address}`);
1773
+ } else if (startupBalance.isLow) {
1774
+ api.logger.warn(
1775
+ `[!] Low balance: ${startupBalance.balanceUSD} remaining. Fund wallet: ${address}`
1776
+ );
1777
+ } else {
1778
+ api.logger.info(`Wallet balance: ${startupBalance.balanceUSD}`);
1779
+ }
1780
+ } catch (err) {
1781
+ api.logger.warn(
1782
+ `Could not check wallet balance: ${err instanceof Error ? err.message : String(err)}`
1783
+ );
1784
+ }
1231
1785
  const routingConfig = api.pluginConfig?.routing;
1232
1786
  const proxy = await startProxy({
1233
1787
  walletKey,
@@ -1242,6 +1796,14 @@ async function startProxyInBackground(api) {
1242
1796
  const cost = decision.costEstimate.toFixed(4);
1243
1797
  const saved = (decision.savings * 100).toFixed(0);
1244
1798
  api.logger.info(`${decision.model} $${cost} (saved ${saved}%)`);
1799
+ },
1800
+ onLowBalance: (info) => {
1801
+ api.logger.warn(`[!] Low balance: ${info.balanceUSD}. Fund wallet: ${info.walletAddress}`);
1802
+ },
1803
+ onInsufficientFunds: (info) => {
1804
+ api.logger.error(
1805
+ `[!] Insufficient funds. Balance: ${info.balanceUSD}, Needed: ${info.requiredUSD}. Fund wallet: ${info.walletAddress}`
1806
+ );
1245
1807
  }
1246
1808
  });
1247
1809
  setActiveProxy(proxy);
@@ -1264,15 +1826,27 @@ var plugin = {
1264
1826
  };
1265
1827
  var index_default = plugin;
1266
1828
  export {
1829
+ BALANCE_THRESHOLDS,
1267
1830
  BLOCKRUN_MODELS,
1831
+ BalanceMonitor,
1832
+ DEFAULT_RETRY_CONFIG,
1268
1833
  DEFAULT_ROUTING_CONFIG,
1834
+ EmptyWalletError,
1835
+ InsufficientFundsError,
1269
1836
  OPENCLAW_MODELS,
1270
1837
  PaymentCache,
1271
1838
  RequestDeduplicator,
1839
+ RpcError,
1272
1840
  blockrunProvider,
1273
1841
  buildProviderModels,
1274
1842
  createPaymentFetch,
1275
1843
  index_default as default,
1844
+ fetchWithRetry,
1845
+ isBalanceError,
1846
+ isEmptyWalletError,
1847
+ isInsufficientFundsError,
1848
+ isRetryable,
1849
+ isRpcError,
1276
1850
  logUsage,
1277
1851
  route,
1278
1852
  startProxy