@blockrun/clawrouter 0.12.62 → 0.12.64

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.
@@ -0,0 +1,559 @@
1
+ # Architecture
2
+
3
+ Technical deep-dive into ClawRouter's internals.
4
+
5
+ ## Table of Contents
6
+
7
+ - [System Overview](#system-overview)
8
+ - [Request Flow](#request-flow)
9
+ - [Routing Engine](#routing-engine)
10
+ - [Payment System](#payment-system)
11
+ - [Optimizations](#optimizations)
12
+ - [Source Structure](#source-structure)
13
+
14
+ ---
15
+
16
+ ## System Overview
17
+
18
+ ```
19
+ ┌─────────────────────────────────────────────────────────────┐
20
+ │ OpenClaw / Your App │
21
+ │ (OpenAI-compatible client) │
22
+ └─────────────────────────────────────────────────────────────┘
23
+
24
+
25
+ ┌─────────────────────────────────────────────────────────────┐
26
+ │ ClawRouter Proxy (localhost) │
27
+ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │
28
+ │ │ Dedup │→ │ Router │→ │ x402 Payment │ │
29
+ │ │ Cache │ │ (15-dim) │ │ (USDC on Base │ │
30
+ │ └─────────────┘ └─────────────┘ │ or Solana) │ │
31
+ │ └───────────────────┘ │
32
+ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │
33
+ │ │ Fallback │ │ Balance │ │ SSE Heartbeat │ │
34
+ │ │ Chain │ │ Monitor │ │ (streaming) │ │
35
+ │ │ │ │ (EVM/Solana)│ │ │ │
36
+ │ └─────────────┘ └─────────────┘ └───────────────────┘ │
37
+ └─────────────────────────────────────────────────────────────┘
38
+
39
+ ┌─────────┴──────────┐
40
+ ▼ ▼
41
+ ┌────────────────────────┐ ┌────────────────────────────────┐
42
+ │ blockrun.ai/api │ │ sol.blockrun.ai/api │
43
+ │ (EVM / Base USDC) │ │ (Solana USDC) │
44
+ │ x402 EIP-712 signing │ │ x402 SVM signing │
45
+ └────────────────────────┘ └────────────────────────────────┘
46
+ │ │
47
+ └──────────────┬────────────────┘
48
+
49
+ OpenAI / Anthropic / Google
50
+ ```
51
+
52
+ **Key Principles:**
53
+
54
+ - **100% local routing** — No API calls for model selection
55
+ - **Client-side only** — Your wallet key never leaves your machine
56
+ - **Non-custodial** — USDC stays in your wallet until spent
57
+ - **Dual-chain** — USDC on Base (EVM) or USDC on Solana; **no SOL token accepted**
58
+
59
+ ---
60
+
61
+ ## Request Flow
62
+
63
+ ### 1. Request Received
64
+
65
+ ```
66
+ POST /v1/chat/completions
67
+ {
68
+ "model": "blockrun/auto",
69
+ "messages": [{ "role": "user", "content": "What is 2+2?" }],
70
+ "stream": true
71
+ }
72
+ ```
73
+
74
+ ### 2. Deduplication Check
75
+
76
+ ```typescript
77
+ // SHA-256 hash of request body
78
+ const dedupKey = RequestDeduplicator.hash(body);
79
+
80
+ // Check completed cache (30s TTL)
81
+ const cached = deduplicator.getCached(dedupKey);
82
+ if (cached) {
83
+ return cached; // Replay cached response
84
+ }
85
+
86
+ // Check in-flight requests
87
+ const inflight = deduplicator.getInflight(dedupKey);
88
+ if (inflight) {
89
+ return await inflight; // Wait for original to complete
90
+ }
91
+ ```
92
+
93
+ ### 3. Smart Routing (if model is `blockrun/auto`)
94
+
95
+ ```typescript
96
+ // Extract user's last message
97
+ const prompt = messages.findLast((m) => m.role === "user")?.content;
98
+
99
+ // Run 14-dimension weighted scorer
100
+ const decision = route(prompt, systemPrompt, maxTokens, {
101
+ config: DEFAULT_ROUTING_CONFIG,
102
+ modelPricing,
103
+ });
104
+
105
+ // decision = {
106
+ // model: "google/gemini-2.5-flash",
107
+ // tier: "SIMPLE",
108
+ // confidence: 0.92,
109
+ // savings: 0.99,
110
+ // costEstimate: 0.0012,
111
+ // }
112
+ ```
113
+
114
+ ### 4. Balance Check
115
+
116
+ ```typescript
117
+ const estimated = estimateAmount(modelId, bodyLength, maxTokens);
118
+ const sufficiency = await balanceMonitor.checkSufficient(estimated);
119
+
120
+ if (sufficiency.info.isEmpty) {
121
+ throw new EmptyWalletError(walletAddress);
122
+ }
123
+
124
+ if (!sufficiency.sufficient) {
125
+ throw new InsufficientFundsError({ ... });
126
+ }
127
+
128
+ if (sufficiency.info.isLow) {
129
+ onLowBalance({ balanceUSD, walletAddress });
130
+ }
131
+ ```
132
+
133
+ ### 5. SSE Heartbeat (for streaming)
134
+
135
+ ```typescript
136
+ if (isStreaming) {
137
+ // Send 200 + headers immediately
138
+ res.writeHead(200, {
139
+ "content-type": "text/event-stream",
140
+ "cache-control": "no-cache",
141
+ });
142
+
143
+ // Heartbeat every 2s to prevent timeout
144
+ heartbeatInterval = setInterval(() => {
145
+ res.write(": heartbeat\n\n");
146
+ }, 2000);
147
+ }
148
+ ```
149
+
150
+ ### 6. x402 Payment Flow
151
+
152
+ **Base (EVM) — EIP-712 USDC:**
153
+
154
+ ```
155
+ 1. Request → blockrun.ai/api
156
+ 2. ← 402 Payment Required
157
+ {
158
+ "x402Version": 1,
159
+ "accepts": [{
160
+ "scheme": "exact",
161
+ "network": "base",
162
+ "maxAmountRequired": "5000", // $0.005 USDC
163
+ "resource": "https://blockrun.ai/api/v1/chat/completions",
164
+ "payTo": "0x..."
165
+ }]
166
+ }
167
+ 3. Sign EIP-712 typed data (EIP-3009 TransferWithAuthorization) with EVM wallet key
168
+ 4. Retry with X-PAYMENT header
169
+ 5. ← 200 OK with response
170
+ ```
171
+
172
+ **Solana — SVM USDC:**
173
+
174
+ ```
175
+ 1. Request → sol.blockrun.ai/api
176
+ 2. ← 402 Payment Required
177
+ {
178
+ "x402Version": 1,
179
+ "accepts": [{
180
+ "scheme": "exact",
181
+ "network": "solana",
182
+ "maxAmountRequired": "5000", // $0.005 USDC (6 decimals)
183
+ "resource": "https://sol.blockrun.ai/api/v1/chat/completions",
184
+ "payTo": "<base58 address>"
185
+ }]
186
+ }
187
+ 3. Build and sign Solana transaction (SPL Token USDC transfer) with Solana wallet key
188
+ - Wallet derived via SLIP-10 Ed25519 (BIP-44 m/44'/501'/0'/0', Phantom-compatible)
189
+ 4. Retry with X-PAYMENT header (base64-encoded signed transaction)
190
+ 5. ← 200 OK with response
191
+ ```
192
+
193
+ > **Important:** Both chains accept only **USDC** tokens. Sending SOL or ETH to the wallet will not fund API payments.
194
+
195
+ ### 7. Fallback Chain (on provider errors)
196
+
197
+ ```typescript
198
+ const FALLBACK_STATUS_CODES = [400, 401, 402, 403, 429, 500, 502, 503, 504];
199
+
200
+ for (const model of fallbackChain) {
201
+ const result = await tryModelRequest(model, ...);
202
+
203
+ if (result.success) {
204
+ return result.response;
205
+ }
206
+
207
+ if (result.isProviderError && !isLastAttempt) {
208
+ console.log(`Fallback: ${model} → next`);
209
+ continue;
210
+ }
211
+
212
+ break;
213
+ }
214
+ ```
215
+
216
+ ### 8. Response Streaming
217
+
218
+ ```typescript
219
+ // Convert non-streaming JSON to SSE format
220
+ // (BlockRun API returns JSON, we simulate SSE)
221
+
222
+ // Chunk 1: role
223
+ data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}
224
+
225
+ // Chunk 2: content
226
+ data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"4"}}]}
227
+
228
+ // Chunk 3: finish
229
+ data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]}
230
+
231
+ data: [DONE]
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Routing Engine
237
+
238
+ ### Weighted Scorer
239
+
240
+ The routing engine uses a 15-dimension weighted scorer that runs entirely locally:
241
+
242
+ ```typescript
243
+ function classifyByRules(
244
+ prompt: string,
245
+ systemPrompt: string | undefined,
246
+ tokenCount: number,
247
+ config: ScoringConfig,
248
+ ): ClassificationResult {
249
+ let score = 0;
250
+ const signals: string[] = [];
251
+
252
+ // Dimension 1: Reasoning markers (weight: 0.18)
253
+ const reasoningCount = countKeywords(prompt, config.reasoningKeywords);
254
+ if (reasoningCount >= 2) {
255
+ score += 0.18 * 2; // Double weight for multiple markers
256
+ signals.push("reasoning");
257
+ }
258
+
259
+ // Dimension 2: Code presence (weight: 0.15)
260
+ if (hasCodeBlock(prompt) || countKeywords(prompt, config.codeKeywords) > 0) {
261
+ score += 0.15;
262
+ signals.push("code");
263
+ }
264
+
265
+ // ... 13 more dimensions
266
+
267
+ // Sigmoid calibration
268
+ const confidence = sigmoid(score, (k = 8), (midpoint = 0.5));
269
+
270
+ return { score, confidence, tier: selectTier(score, confidence), signals };
271
+ }
272
+ ```
273
+
274
+ ### Tier Selection
275
+
276
+ ```typescript
277
+ function selectTier(score: number, confidence: number): Tier | null {
278
+ // Special case: 2+ reasoning markers → REASONING at high confidence
279
+ if (signals.includes("reasoning") && reasoningCount >= 2) {
280
+ return "REASONING";
281
+ }
282
+
283
+ if (confidence < 0.7) {
284
+ return null; // Ambiguous → default to MEDIUM
285
+ }
286
+
287
+ if (score < 0.3) return "SIMPLE";
288
+ if (score < 0.6) return "MEDIUM";
289
+ if (score < 0.8) return "COMPLEX";
290
+ return "REASONING";
291
+ }
292
+ ```
293
+
294
+ ### Overrides
295
+
296
+ Certain conditions force tier assignment:
297
+
298
+ ```typescript
299
+ // Large context → COMPLEX
300
+ if (tokenCount > 100000) {
301
+ return { tier: "COMPLEX", method: "override:large_context" };
302
+ }
303
+
304
+ // Structured output (JSON/YAML) → min MEDIUM
305
+ if (systemPrompt?.includes("json") || systemPrompt?.includes("yaml")) {
306
+ return { tier: Math.max(tier, "MEDIUM"), method: "override:structured" };
307
+ }
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Payment System
313
+
314
+ ### x402 Protocol
315
+
316
+ ClawRouter uses the [x402 protocol](https://x402.org) for micropayments. Both chains use the same flow; the signing step differs:
317
+
318
+ ```
319
+ ┌────────────┐ ┌──────────────────────┐ ┌────────────┐
320
+ │ Client │────▶│ BlockRun API │────▶│ Provider │
321
+ │ (ClawRouter) │ (Base: blockrun.ai │ │ (OpenAI) │
322
+ └────────────┘ │ Sol: sol.blockrun) │ └────────────┘
323
+ │ │
324
+ │ 1. Request │
325
+ │─────────────────▶│
326
+ │ │
327
+ │ 2. 402 + price │
328
+ │◀─────────────────│
329
+ │ │
330
+ │ 3. Sign payment │
331
+ │ Base: EIP-712 │
332
+ │ Solana: SVM tx │
333
+ │ (USDC only) │
334
+ │ │
335
+ │ 4. Retry + sig │
336
+ │─────────────────▶│
337
+ │ │
338
+ │ 5. Response │
339
+ │◀─────────────────│
340
+ ```
341
+
342
+ ### EVM Signing (Base — EIP-712)
343
+
344
+ ```typescript
345
+ const typedData = {
346
+ types: {
347
+ TransferWithAuthorization: [
348
+ { name: "from", type: "address" },
349
+ { name: "to", type: "address" },
350
+ { name: "value", type: "uint256" },
351
+ { name: "validAfter", type: "uint256" },
352
+ { name: "validBefore", type: "uint256" },
353
+ { name: "nonce", type: "bytes32" },
354
+ ],
355
+ },
356
+ primaryType: "TransferWithAuthorization",
357
+ domain: { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: USDC_BASE },
358
+ message: {
359
+ from: walletAddress,
360
+ to: payTo,
361
+ value: BigInt(5000), // 0.005 USDC (6 decimals)
362
+ validAfter: BigInt(0),
363
+ validBefore: BigInt(Math.floor(Date.now() / 1000) + 3600),
364
+ nonce: crypto.getRandomValues(new Uint8Array(32)),
365
+ },
366
+ };
367
+
368
+ const signature = await account.signTypedData(typedData);
369
+ ```
370
+
371
+ ### Solana Signing (SLIP-10 Ed25519)
372
+
373
+ ```typescript
374
+ // Wallet derived via SLIP-10 Ed25519 — Phantom-compatible
375
+ // Path: m/44'/501'/0'/0'
376
+ const solanaAccount = await deriveSlip10Ed25519Key(mnemonic, "m/44'/501'/0'/0'");
377
+
378
+ // Build SPL Token USDC transfer instruction
379
+ const transaction = buildSolanaPaymentTransaction({
380
+ from: solanaAddress,
381
+ to: payTo, // base58 recipient
382
+ mint: USDC_SOLANA, // EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
383
+ amount: BigInt(5000), // 0.005 USDC (6 decimals)
384
+ });
385
+
386
+ const signedTx = await signTransaction(transaction, solanaAccount);
387
+ // Encoded as base64 in X-PAYMENT header
388
+ ```
389
+
390
+ ### Pre-Authorization
391
+
392
+ To skip the 402 round trip:
393
+
394
+ ```typescript
395
+ // Estimate cost before request
396
+ const estimated = estimateAmount(modelId, bodyLength, maxTokens);
397
+
398
+ // Pre-sign payment with estimate (+ 20% buffer)
399
+ const preAuth: PreAuthParams = { estimatedAmount: estimated };
400
+
401
+ // Request with pre-signed payment
402
+ const response = await payFetch(url, init, preAuth);
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Optimizations
408
+
409
+ ### 1. Request Deduplication
410
+
411
+ Prevents double-charging when clients retry after timeout:
412
+
413
+ ```typescript
414
+ class RequestDeduplicator {
415
+ private cache = new Map<string, CachedResponse>();
416
+ private inflight = new Map<string, Promise<CachedResponse>>();
417
+ private TTL_MS = 30_000;
418
+
419
+ static hash(body: Buffer): string {
420
+ return createHash("sha256").update(body).digest("hex");
421
+ }
422
+
423
+ getCached(key: string): CachedResponse | undefined {
424
+ const entry = this.cache.get(key);
425
+ if (entry && Date.now() - entry.completedAt < this.TTL_MS) {
426
+ return entry;
427
+ }
428
+ return undefined;
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### 2. SSE Heartbeat
434
+
435
+ Prevents upstream timeout while waiting for x402 payment:
436
+
437
+ ```
438
+ 0s: Request received
439
+ 0s: → 200 OK, Content-Type: text/event-stream
440
+ 0s: → : heartbeat
441
+ 2s: → : heartbeat (client stays connected)
442
+ 4s: → : heartbeat
443
+ 5s: x402 payment completes
444
+ 5s: → data: {"choices":[...]}
445
+ 5s: → data: [DONE]
446
+ ```
447
+
448
+ ### 3. Balance Caching
449
+
450
+ Avoids RPC calls on every request. Dual-chain monitors are chain-aware:
451
+
452
+ ```typescript
453
+ // EVM monitor (Base): reads USDC balance via eth_call on Base RPC
454
+ class BalanceMonitor {
455
+ private cachedBalance: bigint | undefined;
456
+ private cacheTime = 0;
457
+ private CACHE_TTL_MS = 60_000; // 1 minute
458
+
459
+ async checkBalance(): Promise<BalanceInfo> {
460
+ if (this.cachedBalance !== undefined && Date.now() - this.cacheTime < this.CACHE_TTL_MS) {
461
+ return this.formatBalance(this.cachedBalance);
462
+ }
463
+
464
+ // Fetch USDC balance from Base RPC
465
+ const balance = await this.fetchUSDCBalance(); // ERC-20 balanceOf call
466
+ this.cachedBalance = balance;
467
+ this.cacheTime = Date.now();
468
+ return this.formatBalance(balance);
469
+ }
470
+
471
+ deductEstimated(amount: bigint): void {
472
+ if (this.cachedBalance !== undefined) {
473
+ this.cachedBalance -= amount;
474
+ }
475
+ }
476
+ }
477
+
478
+ // Solana monitor: reads SPL Token USDC balance via getTokenAccountBalance
479
+ class SolanaBalanceMonitor {
480
+ // Same interface as BalanceMonitor — proxy.ts uses AnyBalanceMonitor union type
481
+ // Retries once on empty to handle flaky public RPC endpoints
482
+ // Cache TTL 60s; startup balance never cached (forces fresh read after install)
483
+ }
484
+
485
+ // proxy.ts selects the correct monitor at startup:
486
+ const balanceMonitor: AnyBalanceMonitor =
487
+ paymentChain === "solana"
488
+ ? new SolanaBalanceMonitor(solanaAddress, rpcUrl)
489
+ : new BalanceMonitor(evmAddress, rpcUrl);
490
+ ```
491
+
492
+ ### 4. Proxy Reuse
493
+
494
+ Detects and reuses existing proxy to avoid `EADDRINUSE`:
495
+
496
+ ```typescript
497
+ async function startProxy(options: ProxyOptions): Promise<ProxyHandle> {
498
+ const port = options.port ?? getProxyPort();
499
+
500
+ // Check if proxy already running
501
+ const existingWallet = await checkExistingProxy(port);
502
+ if (existingWallet) {
503
+ // Return handle that uses existing proxy
504
+ return {
505
+ port,
506
+ baseUrl: `http://127.0.0.1:${port}`,
507
+ walletAddress: existingWallet,
508
+ close: async () => {}, // No-op
509
+ };
510
+ }
511
+
512
+ // Start new proxy
513
+ const server = createServer(...);
514
+ server.listen(port, "127.0.0.1");
515
+ // ...
516
+ }
517
+ ```
518
+
519
+ ---
520
+
521
+ ## Source Structure
522
+
523
+ ```
524
+ src/
525
+ ├── index.ts # Plugin entry, OpenClaw integration
526
+ ├── proxy.ts # HTTP proxy server, request handling, chain selection
527
+ ├── provider.ts # OpenClaw provider registration
528
+ ├── models.ts # 41+ model definitions with pricing
529
+ ├── auth.ts # Wallet key resolution (file → env → generate)
530
+ ├── wallet.ts # BIP-39 mnemonic, EVM + Solana key derivation (SLIP-10)
531
+ ├── x402.ts # EVM EIP-712 payment signing, @x402/fetch
532
+ ├── balance.ts # EVM USDC balance monitoring (Base RPC)
533
+ ├── solana-balance.ts # Solana USDC balance monitoring (SPL Token)
534
+ ├── payment-preauth.ts # Pre-authorization caching (EVM only)
535
+ ├── dedup.ts # Request deduplication (SHA-256 → cache)
536
+ ├── logger.ts # JSON usage logging to disk
537
+ ├── errors.ts # Custom error types
538
+ ├── retry.ts # Fetch retry with exponential backoff
539
+ ├── version.ts # Version from package.json
540
+ └── router/
541
+ ├── index.ts # route() entry point
542
+ ├── rules.ts # 15-dimension weighted scorer (9-language)
543
+ ├── selector.ts # Tier → model selection + fallback
544
+ ├── config.ts # Default routing configuration (ECO/AUTO/PREMIUM/AGENTIC)
545
+ └── types.ts # TypeScript type definitions
546
+ ```
547
+
548
+ ### Key Files
549
+
550
+ | File | Purpose |
551
+ | --------------------- | ----------------------------------------------------------- |
552
+ | `proxy.ts` | Core request handling, SSE simulation, fallback chain |
553
+ | `wallet.ts` | BIP-39 mnemonic generation, EVM + Solana (SLIP-10) derivation |
554
+ | `router/rules.ts` | 15-dimension weighted scorer, 9-language keyword sets |
555
+ | `x402.ts` | EIP-712 typed data signing, payment header formatting |
556
+ | `balance.ts` | USDC balance via Base RPC (EVM), caching, thresholds |
557
+ | `solana-balance.ts` | USDC balance via Solana RPC (SPL Token), caching, retries |
558
+ | `payment-preauth.ts` | Pre-authorization cache (EVM; skipped for Solana) |
559
+ | `dedup.ts` | SHA-256 hashing, 30s response cache |