@blockrun/clawrouter 0.12.63 → 0.12.65

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.
Files changed (34) hide show
  1. package/README.md +55 -55
  2. package/dist/cli.js +50 -14
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +57 -16
  5. package/dist/index.js.map +1 -1
  6. package/docs/anthropic-cost-savings.md +90 -85
  7. package/docs/architecture.md +12 -12
  8. package/docs/{blog-openclaw-cost-overruns.md → clawrouter-cuts-llm-api-costs-500x.md} +27 -27
  9. package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +280 -0
  10. package/docs/configuration.md +2 -2
  11. package/docs/image-generation.md +39 -39
  12. package/docs/{blog-benchmark-2026-03.md → llm-router-benchmark-46-models-sub-1ms-routing.md} +61 -64
  13. package/docs/routing-profiles.md +6 -6
  14. package/docs/{technical-routing-2026-03.md → smart-llm-router-14-dimension-classifier.md} +29 -28
  15. package/docs/worker-network.md +438 -347
  16. package/package.json +3 -2
  17. package/scripts/reinstall.sh +31 -6
  18. package/scripts/update.sh +6 -1
  19. package/docs/assets/blockrun-248-day-cost-overrun-problem.png +0 -0
  20. package/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png +0 -0
  21. package/docs/assets/blockrun-clawrouter-observation-compression-97-percent-token-savings.png +0 -0
  22. package/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png +0 -0
  23. package/docs/assets/blockrun-clawrouter-openclaw-automatic-tier-routing-model-selection.png +0 -0
  24. package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png +0 -0
  25. package/docs/assets/blockrun-clawrouter-openclaw-session-memory-journaling-vs-context-compounding.png +0 -0
  26. package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png +0 -0
  27. package/docs/assets/blockrun-clawrouter-x402-usdc-micropayment-wallet-budget-control.png +0 -0
  28. package/docs/assets/blockrun-openclaw-inference-layer-blind-spots.png +0 -0
  29. package/docs/plans/2026-02-03-smart-routing-design.md +0 -267
  30. package/docs/plans/2026-02-13-e2e-docker-deployment.md +0 -1260
  31. package/docs/plans/2026-02-28-worker-network.md +0 -947
  32. package/docs/plans/2026-03-18-error-classification.md +0 -574
  33. package/docs/plans/2026-03-19-exclude-models.md +0 -538
  34. package/docs/vs-openrouter.md +0 -157
@@ -1,947 +0,0 @@
1
- # Worker Network Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Let ClawRouter users opt in as worker nodes that execute HTTP health checks and earn USDC micropayments via x402.
6
-
7
- **Architecture:** ClawRouter polls BlockRun for tasks every 30s, executes HTTP checks, signs results with its existing wallet key, and submits to BlockRun. BlockRun verifies the signature, accumulates credits per worker, and pays out via x402 (TransferWithAuthorization with `payTo = worker address`) when credits hit a $0.01 threshold.
8
-
9
- **Tech Stack:** viem (signing), x402 (payment), existing CDP facilitator (settlement), in-memory task queue (pilot)
10
-
11
- ---
12
-
13
- ## Design Decisions (from architecture discussion)
14
-
15
- ### Verification: Trust-based (no consensus needed)
16
- Workers are existing paying ClawRouter users. The actual work (one HTTP fetch) is cheaper than writing cheating code. Reward is $0.0001 — no rational incentive to fabricate. Simple signature proves identity; that's enough.
17
-
18
- ### Payment: x402 with reversed payTo
19
- x402 is EIP-3009 TransferWithAuthorization. For worker payouts, BlockRun signs as the payer:
20
- - `from: WORKER_PAYOUT_WALLET` (BlockRun treasury)
21
- - `to: workerAddress`
22
- - `value: accumulatedMicros`
23
-
24
- BlockRun calls the existing CDP facilitator `/settle` endpoint. No new payment infrastructure needed.
25
-
26
- ### Payout batching (gas efficiency)
27
- Do NOT pay $0.0001 per check immediately. Accumulate per worker, pay when ≥ $0.01 (100 checks). Base L2 gas ≈ $0.0001/tx → gas overhead = 1%. Acceptable.
28
-
29
- ### Task assignment
30
- With 1000 workers and N tasks, each task is assigned to at most 1 worker per 30s cycle. Simple approach: return tasks that haven't been assigned to THIS worker in the last 5 minutes. Workers in different regions naturally get different results.
31
-
32
- ---
33
-
34
- ## Cost Model
35
-
36
- ### Pilot (3 tasks, 30s interval)
37
- ```
38
- Checks/day: 3 tasks × 2,880 cycles = 8,640 checks
39
- Worker cost: 8,640 × $0.0001 = $0.864/day ≈ $26/month
40
- Gas (batched): ~87 payouts × $0.0001 = $0.009/day ≈ $0.27/month
41
- Total: ~$26/month from treasury
42
- ```
43
-
44
- ### Scale (1,000 tasks, buyers paying)
45
- ```
46
- Worker cost: 1,000 × 2,880 × $0.0001 = $288/day
47
- Buyer price: $0.00011/check (10% margin)
48
- Revenue: 1,000 × 2,880 × $0.00011 = $316.8/day
49
- Net margin: ~$28.8/day on worker network
50
- ```
51
-
52
- ### Worker earnings
53
- ```
54
- 1,000 workers, 1,000 tasks (equal distribution):
55
- Each worker: 1 task/cycle avg
56
- Daily: 2,880 × $0.0001 = $0.288/day
57
- Monthly: ~$8.64/month passive income
58
-
59
- Pilot (1,000 workers, 3 tasks):
60
- Each worker: 0.003 tasks/cycle avg
61
- Daily: ~$0.001/day per worker
62
- → Pilot is about proving the system, not earnings
63
- ```
64
-
65
- ---
66
-
67
- ## Files
68
-
69
- ### ClawRouter (new files)
70
- | File | Action |
71
- |------|--------|
72
- | `src/worker/types.ts` | CREATE |
73
- | `src/worker/checks.ts` | CREATE |
74
- | `src/worker/index.ts` | CREATE |
75
- | `src/index.ts` | MODIFY — add worker startup in `startProxyInBackground()` |
76
-
77
- ### BlockRun (new files)
78
- | File | Action |
79
- |------|--------|
80
- | `src/lib/worker-tasks.ts` | CREATE |
81
- | `src/lib/worker-payouts.ts` | CREATE |
82
- | `src/app/api/v1/worker/tasks/route.ts` | CREATE |
83
- | `src/app/api/v1/worker/results/route.ts` | CREATE |
84
-
85
- ### BlockRun: add health endpoint (needed for self-verification)
86
- | File | Action |
87
- |------|--------|
88
- | `src/app/api/health/route.ts` | CHECK if exists — if not, CREATE |
89
-
90
- ---
91
-
92
- ## Task 1: ClawRouter — Worker Types
93
-
94
- **Files:**
95
- - Create: `src/worker/types.ts`
96
-
97
- ```typescript
98
- export interface WorkerTask {
99
- id: string
100
- type: "http_check"
101
- url: string
102
- expectedStatus: number
103
- timeoutMs: number
104
- rewardMicros: number
105
- region?: string
106
- }
107
-
108
- export interface WorkerResult {
109
- taskId: string
110
- workerAddress: string
111
- timestamp: number
112
- success: boolean
113
- responseTimeMs: number
114
- statusCode?: number
115
- error?: string
116
- // EIP-191 signature of JSON.stringify({ taskId, workerAddress, timestamp, success })
117
- signature: string
118
- }
119
-
120
- export interface WorkerStatus {
121
- address: string
122
- completedTasks: number
123
- totalEarnedMicros: number
124
- lastPollAt?: number
125
- busy: boolean
126
- }
127
- ```
128
-
129
- **Step 1:** Create `src/worker/` directory and `types.ts` with content above.
130
-
131
- **Step 2:** Commit
132
- ```bash
133
- git add src/worker/types.ts
134
- git commit -m "feat(worker): add worker network types"
135
- ```
136
-
137
- ---
138
-
139
- ## Task 2: ClawRouter — HTTP Check Executor
140
-
141
- **Files:**
142
- - Create: `src/worker/checks.ts`
143
-
144
- ```typescript
145
- import type { WorkerTask } from "./types.js"
146
-
147
- export async function executeHttpCheck(task: WorkerTask): Promise<{
148
- success: boolean
149
- responseTimeMs: number
150
- statusCode?: number
151
- error?: string
152
- }> {
153
- const start = Date.now()
154
- try {
155
- const response = await fetch(task.url, {
156
- method: "GET",
157
- signal: AbortSignal.timeout(task.timeoutMs),
158
- redirect: "follow",
159
- headers: { "User-Agent": "BlockRun-Worker/1.0" },
160
- })
161
- return {
162
- success: response.status === task.expectedStatus,
163
- responseTimeMs: Date.now() - start,
164
- statusCode: response.status,
165
- }
166
- } catch (err) {
167
- const isTimeout = err instanceof Error &&
168
- (err.name === "TimeoutError" || err.name === "AbortError")
169
- return {
170
- success: false,
171
- responseTimeMs: Date.now() - start,
172
- error: isTimeout ? `Timeout after ${task.timeoutMs}ms` : String(err instanceof Error ? err.message : err),
173
- }
174
- }
175
- }
176
-
177
- // Both sides must produce identical JSON for signature verification
178
- export function buildSignableMessage(params: {
179
- taskId: string
180
- workerAddress: string
181
- timestamp: number
182
- success: boolean
183
- }): string {
184
- return JSON.stringify({
185
- taskId: params.taskId,
186
- workerAddress: params.workerAddress,
187
- timestamp: params.timestamp,
188
- success: params.success,
189
- })
190
- }
191
- ```
192
-
193
- **Step 1:** Create `src/worker/checks.ts` with content above.
194
-
195
- **Step 2:** Commit
196
- ```bash
197
- git add src/worker/checks.ts
198
- git commit -m "feat(worker): add HTTP check executor"
199
- ```
200
-
201
- ---
202
-
203
- ## Task 3: ClawRouter — WorkerNode Class
204
-
205
- **Files:**
206
- - Create: `src/worker/index.ts`
207
-
208
- Key imports needed: `privateKeyToAccount` from `viem/accounts` (already a dependency).
209
-
210
- ```typescript
211
- import { privateKeyToAccount } from "viem/accounts"
212
- import type { WorkerTask, WorkerResult, WorkerStatus } from "./types.js"
213
- import { executeHttpCheck, buildSignableMessage } from "./checks.js"
214
-
215
- const BLOCKRUN_API = "https://blockrun.ai/api"
216
- const POLL_INTERVAL_MS = 30_000
217
- const MAX_CONCURRENT_CHECKS = 10
218
- const REGION = process.env.WORKER_REGION || "unknown"
219
-
220
- export class WorkerNode {
221
- private address: string
222
- private privateKey: `0x${string}`
223
- private apiBase: string
224
- private status: WorkerStatus
225
- private pollTimer?: ReturnType<typeof setInterval>
226
- private busy = false
227
-
228
- constructor(walletKey: string, walletAddress: string, apiBase = BLOCKRUN_API) {
229
- this.privateKey = walletKey as `0x${string}`
230
- this.address = walletAddress
231
- this.apiBase = apiBase
232
- this.status = {
233
- address: walletAddress,
234
- completedTasks: 0,
235
- totalEarnedMicros: 0,
236
- busy: false,
237
- }
238
- }
239
-
240
- startPolling(): void {
241
- console.log(`[Worker] Starting — address: ${this.address}, region: ${REGION}`)
242
- // Run immediately, then on interval
243
- this.poll().catch(console.error)
244
- this.pollTimer = setInterval(() => {
245
- this.poll().catch(console.error)
246
- }, POLL_INTERVAL_MS)
247
- }
248
-
249
- stopPolling(): void {
250
- if (this.pollTimer) {
251
- clearInterval(this.pollTimer)
252
- this.pollTimer = undefined
253
- }
254
- }
255
-
256
- getStatus(): WorkerStatus {
257
- return { ...this.status, busy: this.busy }
258
- }
259
-
260
- private async poll(): Promise<void> {
261
- if (this.busy) return // skip cycle if still executing
262
-
263
- let tasks: WorkerTask[] = []
264
- try {
265
- tasks = await this.fetchTasks()
266
- } catch (err) {
267
- console.error(`[Worker] Failed to fetch tasks:`, err instanceof Error ? err.message : err)
268
- return
269
- }
270
-
271
- if (tasks.length === 0) return
272
-
273
- this.busy = true
274
- this.status.lastPollAt = Date.now()
275
- console.log(`[Worker] Executing ${tasks.length} task(s)`)
276
-
277
- try {
278
- const results = await this.executeBatch(tasks)
279
- await this.submitResults(results)
280
- } finally {
281
- this.busy = false
282
- }
283
- }
284
-
285
- private async fetchTasks(): Promise<WorkerTask[]> {
286
- const url = `${this.apiBase}/v1/worker/tasks?address=${this.address}&region=${REGION}`
287
- const res = await fetch(url, {
288
- signal: AbortSignal.timeout(10_000),
289
- headers: { "User-Agent": "BlockRun-Worker/1.0" },
290
- })
291
- if (!res.ok) {
292
- throw new Error(`Tasks endpoint returned ${res.status}`)
293
- }
294
- return res.json() as Promise<WorkerTask[]>
295
- }
296
-
297
- private async executeBatch(tasks: WorkerTask[]): Promise<WorkerResult[]> {
298
- // Cap concurrency at MAX_CONCURRENT_CHECKS
299
- const chunks: WorkerTask[][] = []
300
- for (let i = 0; i < tasks.length; i += MAX_CONCURRENT_CHECKS) {
301
- chunks.push(tasks.slice(i, i + MAX_CONCURRENT_CHECKS))
302
- }
303
-
304
- const results: WorkerResult[] = []
305
- for (const chunk of chunks) {
306
- const chunkResults = await Promise.all(chunk.map(task => this.executeAndSign(task)))
307
- results.push(...chunkResults)
308
- }
309
- return results
310
- }
311
-
312
- private async executeAndSign(task: WorkerTask): Promise<WorkerResult> {
313
- const check = await executeHttpCheck(task)
314
- const timestamp = Date.now()
315
-
316
- const message = buildSignableMessage({
317
- taskId: task.id,
318
- workerAddress: this.address,
319
- timestamp,
320
- success: check.success,
321
- })
322
-
323
- const account = privateKeyToAccount(this.privateKey)
324
- const signature = await account.signMessage({ message })
325
-
326
- return {
327
- taskId: task.id,
328
- workerAddress: this.address,
329
- timestamp,
330
- ...check,
331
- signature,
332
- }
333
- }
334
-
335
- private async submitResults(results: WorkerResult[]): Promise<void> {
336
- try {
337
- const res = await fetch(`${this.apiBase}/v1/worker/results`, {
338
- method: "POST",
339
- headers: {
340
- "Content-Type": "application/json",
341
- "User-Agent": "BlockRun-Worker/1.0",
342
- },
343
- body: JSON.stringify(results),
344
- signal: AbortSignal.timeout(15_000),
345
- })
346
-
347
- if (!res.ok) {
348
- console.error(`[Worker] Results submission failed: ${res.status}`)
349
- return
350
- }
351
-
352
- const data = await res.json() as { accepted: number; earned: string }
353
- this.status.completedTasks += data.accepted
354
- console.log(`[Worker] Submitted ${data.accepted} result(s), earned: $${data.earned} USDC`)
355
- } catch (err) {
356
- console.error(`[Worker] Failed to submit results:`, err instanceof Error ? err.message : err)
357
- }
358
- }
359
- }
360
- ```
361
-
362
- **Step 1:** Create `src/worker/index.ts` with content above.
363
-
364
- **Step 2:** Verify TypeScript compiles
365
- ```bash
366
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter
367
- npx tsc --noEmit
368
- ```
369
- Expected: no errors in worker files.
370
-
371
- **Step 3:** Commit
372
- ```bash
373
- git add src/worker/index.ts
374
- git commit -m "feat(worker): add WorkerNode class with polling and signing"
375
- ```
376
-
377
- ---
378
-
379
- ## Task 4: ClawRouter — Wire Worker Mode in index.ts
380
-
381
- **Files:**
382
- - Modify: `src/index.ts` — inside `startProxyInBackground()`, after `setActiveProxy(proxy)`
383
-
384
- Find this line in `startProxyInBackground()` (~line 423):
385
- ```typescript
386
- setActiveProxy(proxy);
387
- activeProxyHandle = proxy;
388
- ```
389
-
390
- Add immediately after:
391
- ```typescript
392
- // Worker mode: opt-in via CLAWROUTER_WORKER=1 or --worker flag
393
- const workerMode =
394
- process.env.CLAWROUTER_WORKER === "1" ||
395
- process.argv.includes("--worker")
396
-
397
- if (workerMode) {
398
- const { WorkerNode } = await import("./worker/index.js")
399
- const worker = new WorkerNode(walletKey, address)
400
- worker.startPolling()
401
- api.logger.info(`[Worker] Mode active — polling for tasks every 30s`)
402
- api.logger.info(`[Worker] Wallet: ${address}`)
403
- }
404
- ```
405
-
406
- **Step 1:** Apply the edit above to `src/index.ts`.
407
-
408
- **Step 2:** Verify TypeScript compiles
409
- ```bash
410
- npx tsc --noEmit
411
- ```
412
-
413
- **Step 3:** Commit
414
- ```bash
415
- git add src/index.ts
416
- git commit -m "feat(worker): activate WorkerNode when CLAWROUTER_WORKER=1"
417
- ```
418
-
419
- ---
420
-
421
- ## Task 5: BlockRun — Worker Task Registry
422
-
423
- **Files:**
424
- - Create: `src/lib/worker-tasks.ts`
425
-
426
- ```typescript
427
- import type { WorkerTask } from "./worker-types"
428
-
429
- // Re-export types so routes can import from one place
430
- export type { WorkerTask }
431
-
432
- export interface WorkerTask {
433
- id: string
434
- type: "http_check"
435
- url: string
436
- expectedStatus: number
437
- timeoutMs: number
438
- rewardMicros: number
439
- region?: string
440
- }
441
-
442
- // Pilot seed tasks — BlockRun-owned endpoints, verifiable and real
443
- export const PILOT_TASKS: WorkerTask[] = [
444
- {
445
- id: "task_br_health",
446
- type: "http_check",
447
- url: "https://blockrun.ai/api/health",
448
- expectedStatus: 200,
449
- timeoutMs: 10_000,
450
- rewardMicros: 100, // $0.0001
451
- },
452
- {
453
- id: "task_br_models",
454
- type: "http_check",
455
- url: "https://blockrun.ai/api/v1/models",
456
- expectedStatus: 200,
457
- timeoutMs: 10_000,
458
- rewardMicros: 100,
459
- },
460
- {
461
- id: "task_base_rpc",
462
- type: "http_check",
463
- url: "https://mainnet.base.org",
464
- expectedStatus: 200,
465
- timeoutMs: 10_000,
466
- rewardMicros: 150, // slightly higher for third-party
467
- },
468
- ]
469
-
470
- // Track last assignment: taskId → { workerAddress, assignedAt }
471
- const lastAssignments = new Map<string, { workerAddress: string; assignedAt: number }>()
472
-
473
- // How long before a task can be reassigned to the same worker
474
- const REASSIGN_COOLDOWN_MS = 5 * 60 * 1000 // 5 minutes
475
-
476
- /**
477
- * Return tasks that haven't been assigned to this worker recently.
478
- * This ensures different workers get different tasks, providing geographic diversity.
479
- */
480
- export function getTasksForWorker(workerAddress: string, _region?: string): WorkerTask[] {
481
- const now = Date.now()
482
- const address = workerAddress.toLowerCase()
483
-
484
- return PILOT_TASKS.filter(task => {
485
- const last = lastAssignments.get(task.id)
486
- if (!last) return true // never assigned
487
- // Reassign if cooldown expired OR it was assigned to a different worker
488
- if (now - last.assignedAt > REASSIGN_COOLDOWN_MS) return true
489
- if (last.workerAddress !== address) return true
490
- return false
491
- })
492
- }
493
-
494
- /**
495
- * Mark tasks as assigned to a worker (called when GET /tasks responds).
496
- */
497
- export function markTasksAssigned(taskIds: string[], workerAddress: string): void {
498
- const now = Date.now()
499
- const address = workerAddress.toLowerCase()
500
- for (const id of taskIds) {
501
- lastAssignments.set(id, { workerAddress: address, assignedAt: now })
502
- }
503
- }
504
-
505
- /**
506
- * Look up a task by ID.
507
- */
508
- export function getTaskById(taskId: string): WorkerTask | undefined {
509
- return PILOT_TASKS.find(t => t.id === taskId)
510
- }
511
- ```
512
-
513
- **Step 1:** Create `src/lib/worker-tasks.ts` with content above.
514
-
515
- **Step 2:** Commit
516
- ```bash
517
- cd /Users/vickyfu/Documents/blockrun-web/blockrun
518
- git add src/lib/worker-tasks.ts
519
- git commit -m "feat(worker): add task registry with pilot seed tasks"
520
- ```
521
-
522
- ---
523
-
524
- ## Task 6: BlockRun — Worker Payout Module
525
-
526
- **Files:**
527
- - Create: `src/lib/worker-payouts.ts`
528
-
529
- This module accumulates credits per worker and triggers x402-style USDC payouts when threshold is reached. Uses the same EIP-3009 signing infrastructure as the rest of BlockRun.
530
-
531
- ```typescript
532
- import { signTypedData, privateKeyToAccount } from "viem/accounts"
533
- import { getCurrentNetworkConfig } from "./network-config"
534
- import { settlePaymentWithRetry } from "./x402"
535
-
536
- // Minimum payout threshold — accumulate before paying to save gas
537
- const PAYOUT_THRESHOLD_MICROS = 10_000 // $0.01
538
-
539
- // In-memory credit ledger: workerAddress → accumulatedMicros
540
- const credits = new Map<string, number>()
541
-
542
- /**
543
- * Add earned micros for a worker. Triggers payout if threshold reached.
544
- * Returns the amount paid out (0 if threshold not yet reached).
545
- */
546
- export async function creditWorker(
547
- workerAddress: string,
548
- earnedMicros: number
549
- ): Promise<{ paid: number; txHash?: string }> {
550
- const address = workerAddress.toLowerCase()
551
- const current = credits.get(address) ?? 0
552
- const newTotal = current + earnedMicros
553
- credits.set(address, newTotal)
554
-
555
- if (newTotal < PAYOUT_THRESHOLD_MICROS) {
556
- return { paid: 0 }
557
- }
558
-
559
- // Threshold reached — trigger payout
560
- credits.set(address, 0) // reset before async to avoid double-pay
561
-
562
- try {
563
- const result = await sendUsdcToWorker(workerAddress as `0x${string}`, newTotal)
564
- console.log(`[Worker Payout] Sent ${newTotal} micros ($${(newTotal / 1_000_000).toFixed(6)}) to ${workerAddress}`)
565
- return { paid: newTotal, txHash: result.txHash }
566
- } catch (err) {
567
- // Restore credits on failure — worker doesn't lose their earnings
568
- credits.set(address, newTotal)
569
- console.error(`[Worker Payout] Failed to pay ${workerAddress}:`, err)
570
- throw err
571
- }
572
- }
573
-
574
- /**
575
- * Get pending (not yet paid) credits for a worker.
576
- */
577
- export function getPendingCredits(workerAddress: string): number {
578
- return credits.get(workerAddress.toLowerCase()) ?? 0
579
- }
580
-
581
- /**
582
- * Send USDC from BlockRun payout wallet to worker.
583
- * Uses same EIP-3009 TransferWithAuthorization as x402, but BlockRun is the payer.
584
- */
585
- async function sendUsdcToWorker(
586
- workerAddress: `0x${string}`,
587
- amountMicros: number
588
- ): Promise<{ txHash?: string }> {
589
- const payoutKey = process.env.WORKER_PAYOUT_WALLET_KEY
590
-
591
- if (!payoutKey || !payoutKey.startsWith("0x")) {
592
- throw new Error("WORKER_PAYOUT_WALLET_KEY not configured")
593
- }
594
-
595
- const networkConfig = getCurrentNetworkConfig()
596
- const payoutAccount = privateKeyToAccount(payoutKey as `0x${string}`)
597
-
598
- const validAfter = BigInt(0)
599
- const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour
600
- const nonceBytes = crypto.getRandomValues(new Uint8Array(32))
601
- const nonce = `0x${Buffer.from(nonceBytes).toString("hex")}` as `0x${string}`
602
-
603
- // Sign TransferWithAuthorization: BlockRun treasury → worker
604
- // Same scheme as ClawRouter's x402.ts but payTo = worker address
605
- const signature = await signTypedData({
606
- privateKey: payoutKey as `0x${string}`,
607
- domain: {
608
- name: networkConfig.usdcDomainName,
609
- version: "2",
610
- chainId: networkConfig.chainId,
611
- verifyingContract: networkConfig.usdc,
612
- },
613
- types: {
614
- TransferWithAuthorization: [
615
- { name: "from", type: "address" },
616
- { name: "to", type: "address" },
617
- { name: "value", type: "uint256" },
618
- { name: "validAfter", type: "uint256" },
619
- { name: "validBefore", type: "uint256" },
620
- { name: "nonce", type: "bytes32" },
621
- ],
622
- },
623
- primaryType: "TransferWithAuthorization",
624
- message: {
625
- from: payoutAccount.address,
626
- to: workerAddress,
627
- value: BigInt(amountMicros),
628
- validAfter,
629
- validBefore,
630
- nonce,
631
- },
632
- })
633
-
634
- // Build payment requirements for CDP facilitator (same shape as incoming payments)
635
- const requirements = {
636
- scheme: "exact",
637
- network: networkConfig.network,
638
- maxAmountRequired: String(amountMicros),
639
- resource: `worker-payout:${workerAddress}`,
640
- description: "Worker node payout",
641
- mimeType: "application/json",
642
- payTo: workerAddress,
643
- maxTimeoutSeconds: 3600,
644
- asset: networkConfig.usdc,
645
- outputSchema: null,
646
- extra: null,
647
- }
648
-
649
- // Build payment header in x402 format
650
- const paymentPayload = {
651
- x402Version: 1,
652
- scheme: "exact",
653
- network: networkConfig.network,
654
- payload: {
655
- signature,
656
- authorization: {
657
- from: payoutAccount.address,
658
- to: workerAddress,
659
- value: String(amountMicros),
660
- validAfter: String(validAfter),
661
- validBefore: String(validBefore),
662
- nonce,
663
- },
664
- },
665
- }
666
- const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64")
667
-
668
- const result = await settlePaymentWithRetry(paymentHeader, requirements as never)
669
-
670
- if (!result.success) {
671
- throw new Error(`Settlement failed: ${result.error}`)
672
- }
673
-
674
- return { txHash: result.txHash }
675
- }
676
- ```
677
-
678
- **Note on `networkConfig.chainId`:** Check `src/lib/network-config.ts` — if `chainId` is not in the `NetworkConfig` type, add it: mainnet = 8453, testnet = 84532.
679
-
680
- **Step 1:** Check network-config.ts for `chainId` field. If missing, add it.
681
-
682
- **Step 2:** Create `src/lib/worker-payouts.ts` with content above.
683
-
684
- **Step 3:** Verify TypeScript
685
- ```bash
686
- npx tsc --noEmit 2>&1 | grep worker
687
- ```
688
-
689
- **Step 4:** Commit
690
- ```bash
691
- git add src/lib/worker-payouts.ts src/lib/network-config.ts
692
- git commit -m "feat(worker): add payout module with batched x402 USDC transfers"
693
- ```
694
-
695
- ---
696
-
697
- ## Task 7: BlockRun — GET /api/v1/worker/tasks
698
-
699
- **Files:**
700
- - Create: `src/app/api/v1/worker/tasks/route.ts`
701
-
702
- ```typescript
703
- import { NextRequest, NextResponse } from "next/server"
704
- import { getTasksForWorker, markTasksAssigned } from "@/lib/worker-tasks"
705
-
706
- export const runtime = "nodejs"
707
-
708
- export async function GET(request: NextRequest) {
709
- const { searchParams } = new URL(request.url)
710
- const address = searchParams.get("address")
711
- const region = searchParams.get("region") ?? undefined
712
-
713
- if (!address || !address.startsWith("0x")) {
714
- return NextResponse.json({ error: "address required" }, { status: 400 })
715
- }
716
-
717
- const tasks = getTasksForWorker(address, region)
718
- markTasksAssigned(tasks.map(t => t.id), address)
719
-
720
- return NextResponse.json(tasks)
721
- }
722
- ```
723
-
724
- **Step 1:** Create directory `src/app/api/v1/worker/tasks/` and `route.ts` with content above.
725
-
726
- **Step 2:** Test manually
727
- ```bash
728
- curl "http://localhost:3000/api/v1/worker/tasks?address=0x1234567890123456789012345678901234567890"
729
- ```
730
- Expected: JSON array with 3 pilot tasks.
731
-
732
- **Step 3:** Commit
733
- ```bash
734
- git add src/app/api/v1/worker/tasks/route.ts
735
- git commit -m "feat(worker): add GET /api/v1/worker/tasks endpoint"
736
- ```
737
-
738
- ---
739
-
740
- ## Task 8: BlockRun — POST /api/v1/worker/results
741
-
742
- **Files:**
743
- - Create: `src/app/api/v1/worker/results/route.ts`
744
-
745
- ```typescript
746
- import { NextRequest, NextResponse } from "next/server"
747
- import { recoverMessageAddress } from "viem"
748
- import { getTaskById } from "@/lib/worker-tasks"
749
- import { creditWorker } from "@/lib/worker-payouts"
750
- import { logWorkerResult } from "@/lib/gcs-logger"
751
-
752
- export const runtime = "nodejs"
753
-
754
- interface WorkerResult {
755
- taskId: string
756
- workerAddress: string
757
- timestamp: number
758
- success: boolean
759
- responseTimeMs: number
760
- statusCode?: number
761
- error?: string
762
- signature: string
763
- }
764
-
765
- export async function POST(request: NextRequest) {
766
- let results: WorkerResult[]
767
-
768
- try {
769
- results = await request.json()
770
- } catch {
771
- return NextResponse.json({ error: "invalid JSON" }, { status: 400 })
772
- }
773
-
774
- if (!Array.isArray(results) || results.length === 0) {
775
- return NextResponse.json({ error: "results must be non-empty array" }, { status: 400 })
776
- }
777
-
778
- // Cap batch size
779
- if (results.length > 50) {
780
- return NextResponse.json({ error: "max 50 results per batch" }, { status: 400 })
781
- }
782
-
783
- let accepted = 0
784
- let totalEarnedMicros = 0
785
- const errors: string[] = []
786
-
787
- for (const result of results) {
788
- try {
789
- // 1. Look up task
790
- const task = getTaskById(result.taskId)
791
- if (!task) {
792
- errors.push(`${result.taskId}: unknown task`)
793
- continue
794
- }
795
-
796
- // 2. Verify EIP-191 signature
797
- const message = JSON.stringify({
798
- taskId: result.taskId,
799
- workerAddress: result.workerAddress,
800
- timestamp: result.timestamp,
801
- success: result.success,
802
- })
803
-
804
- const recovered = await recoverMessageAddress({
805
- message,
806
- signature: result.signature as `0x${string}`,
807
- })
808
-
809
- if (recovered.toLowerCase() !== result.workerAddress.toLowerCase()) {
810
- errors.push(`${result.taskId}: invalid signature`)
811
- continue
812
- }
813
-
814
- // 3. Sanity checks
815
- const age = Date.now() - result.timestamp
816
- if (age > 5 * 60 * 1000) {
817
- errors.push(`${result.taskId}: result too old (${Math.round(age / 1000)}s)`)
818
- continue
819
- }
820
-
821
- // 4. Log to GCS (fire and forget)
822
- logWorkerResult({
823
- taskId: result.taskId,
824
- workerAddress: result.workerAddress,
825
- timestamp: result.timestamp,
826
- success: result.success,
827
- responseTimeMs: result.responseTimeMs,
828
- statusCode: result.statusCode,
829
- rewardMicros: task.rewardMicros,
830
- }).catch(console.error)
831
-
832
- // 5. Credit worker — triggers payout if threshold reached
833
- await creditWorker(result.workerAddress, task.rewardMicros)
834
-
835
- accepted++
836
- totalEarnedMicros += task.rewardMicros
837
-
838
- } catch (err) {
839
- errors.push(`${result.taskId}: ${err instanceof Error ? err.message : String(err)}`)
840
- }
841
- }
842
-
843
- return NextResponse.json({
844
- accepted,
845
- earned: (totalEarnedMicros / 1_000_000).toFixed(6), // in USDC
846
- errors: errors.length > 0 ? errors : undefined,
847
- })
848
- }
849
- ```
850
-
851
- **Step 1:** Create `src/app/api/v1/worker/results/route.ts` with content above.
852
-
853
- **Step 2:** Add `logWorkerResult` to `src/lib/gcs-logger.ts`:
854
- ```typescript
855
- export async function logWorkerResult(result: {
856
- taskId: string
857
- workerAddress: string
858
- timestamp: number
859
- success: boolean
860
- responseTimeMs: number
861
- statusCode?: number
862
- rewardMicros: number
863
- }): Promise<void> {
864
- const date = new Date(result.timestamp).toISOString().split("T")[0]
865
- const fileName = `worker-results/${date}/${result.taskId}-${result.timestamp}.json`
866
- // Use same GCS write pattern as logLLMCall
867
- try {
868
- const file = bucket.file(fileName)
869
- await file.save(JSON.stringify(result), { contentType: "application/json" })
870
- } catch {
871
- // GCS failure should not block payment
872
- }
873
- }
874
- ```
875
-
876
- **Step 3:** Verify TypeScript
877
- ```bash
878
- npx tsc --noEmit
879
- ```
880
-
881
- **Step 4:** Commit
882
- ```bash
883
- git add src/app/api/v1/worker/results/route.ts src/lib/gcs-logger.ts
884
- git commit -m "feat(worker): add POST /api/v1/worker/results with sig verify and payout"
885
- ```
886
-
887
- ---
888
-
889
- ## Task 9: Environment Variables
890
-
891
- **ClawRouter** (no new env vars needed for basic mode):
892
- ```bash
893
- CLAWROUTER_WORKER=1 # opt-in to worker mode
894
- WORKER_REGION=US-West # optional geographic tag
895
- ```
896
-
897
- **BlockRun** (add to `.env.local` and Cloud Run secrets):
898
- ```bash
899
- WORKER_PAYOUT_WALLET_KEY=0x... # treasury key for paying workers
900
- ```
901
-
902
- **Step 1:** Add `WORKER_PAYOUT_WALLET_KEY` to BlockRun's `.env.local.example` (never commit real key).
903
-
904
- **Step 2:** Document in BlockRun's README or deployment notes.
905
-
906
- ---
907
-
908
- ## Testing the Pilot End-to-End
909
-
910
- **Step 1:** Start BlockRun dev server
911
- ```bash
912
- cd /Users/vickyfu/Documents/blockrun-web/blockrun
913
- npm run dev
914
- ```
915
-
916
- **Step 2:** Verify tasks endpoint
917
- ```bash
918
- curl "http://localhost:3000/api/v1/worker/tasks?address=0x0000000000000000000000000000000000000001"
919
- # Expected: [{id: "task_br_health", ...}, ...]
920
- ```
921
-
922
- **Step 3:** Start ClawRouter in worker mode (pointing at localhost)
923
- ```bash
924
- cd /Users/vickyfu/Documents/blockrun-web/ClawRouter
925
- CLAWROUTER_WORKER=1 BLOCKRUN_API_BASE=http://localhost:3000/api npx openclaw gateway start
926
- ```
927
-
928
- **Note:** `BLOCKRUN_API_BASE` override needs to be wired into `WorkerNode` constructor — add support for this env var.
929
-
930
- **Step 4:** Watch logs for
931
- ```
932
- [Worker] Starting — address: 0x...
933
- [Worker] Executing 3 task(s)
934
- [Worker] Submitted 3 result(s), earned: $0.000300 USDC
935
- ```
936
-
937
- **Step 5:** Check BlockRun logs for incoming results and payout trigger at $0.01 threshold.
938
-
939
- ---
940
-
941
- ## Open Questions / V2
942
-
943
- - Worker registration portal (let users explicitly opt in via UI)
944
- - Buyer dashboard (custom endpoint monitoring)
945
- - Geographic routing (assign tasks by region)
946
- - Slash mechanism if needed at scale (currently not needed)
947
- - `/wallet worker-status` command in ClawRouter to show earnings