@blockrun/clawrouter 0.12.62 → 0.12.63
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/docs/anthropic-cost-savings.md +349 -0
- package/docs/architecture.md +559 -0
- package/docs/assets/blockrun-248-day-cost-overrun-problem.png +0 -0
- package/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png +0 -0
- package/docs/assets/blockrun-clawrouter-observation-compression-97-percent-token-savings.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-automatic-tier-routing-model-selection.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png +0 -0
- package/docs/assets/blockrun-clawrouter-openclaw-session-memory-journaling-vs-context-compounding.png +0 -0
- package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png +0 -0
- package/docs/assets/blockrun-clawrouter-x402-usdc-micropayment-wallet-budget-control.png +0 -0
- package/docs/assets/blockrun-openclaw-inference-layer-blind-spots.png +0 -0
- package/docs/blog-benchmark-2026-03.md +184 -0
- package/docs/blog-openclaw-cost-overruns.md +197 -0
- package/docs/clawrouter-savings.png +0 -0
- package/docs/configuration.md +512 -0
- package/docs/features.md +257 -0
- package/docs/image-generation.md +380 -0
- package/docs/plans/2026-02-03-smart-routing-design.md +267 -0
- package/docs/plans/2026-02-13-e2e-docker-deployment.md +1260 -0
- package/docs/plans/2026-02-28-worker-network.md +947 -0
- package/docs/plans/2026-03-18-error-classification.md +574 -0
- package/docs/plans/2026-03-19-exclude-models.md +538 -0
- package/docs/routing-profiles.md +81 -0
- package/docs/subscription-failover.md +320 -0
- package/docs/technical-routing-2026-03.md +322 -0
- package/docs/troubleshooting.md +159 -0
- package/docs/vision.md +49 -0
- package/docs/vs-openrouter.md +157 -0
- package/docs/worker-network.md +1241 -0
- package/package.json +2 -1
|
@@ -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 |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png
ADDED
|
Binary file
|
|
Binary file
|
package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|