@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
@@ -21,9 +21,11 @@ ClawRouter Worker Mode transforms any ClawRouter installation into a node in a d
21
21
  ## Target Customers
22
22
 
23
23
  ### Primary: Web3 Protocols (Phase 1)
24
+
24
25
  Blockchain protocols, L1/L2 chains, DeFi applications, RPC providers.
25
26
 
26
27
  **Why they buy:**
28
+
27
29
  - CEX listing requirements mandate uptime SLA proof
28
30
  - Institutional investors require auditable availability records
29
31
  - Decentralized proof (multi-node, on-chain payment trail) is more credible than self-reported metrics
@@ -32,9 +34,11 @@ Blockchain protocols, L1/L2 chains, DeFi applications, RPC providers.
32
34
  **Example customers:** New L2 chains seeking Binance/Coinbase listing, DeFi protocols pitching institutional LPs, bridge protocols, oracle networks
33
35
 
34
36
  ### Secondary: AI API Providers (Phase 2)
37
+
35
38
  OpenAI, Anthropic, and the long tail of AI API businesses.
36
39
 
37
40
  ### Tertiary: SaaS & Fintech (Phase 2+)
41
+
38
42
  Any B2B company that sells to enterprises or operates under financial regulation.
39
43
 
40
44
  ---
@@ -43,13 +47,14 @@ Any B2B company that sells to enterprises or operates under financial regulation
43
47
 
44
48
  ### For Buyers
45
49
 
46
- | Tier | SLA | Price | BlockRun margin |
47
- |------|-----|-------|----------------|
48
- | **Best Effort** | Checks run when workers online (~90% coverage) | $0.0003/check | 67% |
49
- | **Standard** | ≥1 check/min guaranteed (BlockRun fills gaps) | $0.001/check | 90% |
50
- | **Premium** | 30s guaranteed + multi-region report | $0.003/check | 97% |
50
+ | Tier | SLA | Price | BlockRun margin |
51
+ | --------------- | ---------------------------------------------- | ------------- | --------------- |
52
+ | **Best Effort** | Checks run when workers online (~90% coverage) | $0.0003/check | 67% |
53
+ | **Standard** | ≥1 check/min guaranteed (BlockRun fills gaps) | $0.001/check | 90% |
54
+ | **Premium** | 30s guaranteed + multi-region report | $0.003/check | 97% |
51
55
 
52
56
  Monthly equivalent per endpoint (30s Standard):
57
+
53
58
  - 2,880 checks/day × 30 × $0.001 = **$86.40/month**
54
59
  - Worker cost: 2,880 × $0.0001 = **$8.64/month**
55
60
  - **BlockRun margin: $77.76/endpoint/month**
@@ -75,12 +80,12 @@ BlockRun already has all payment data from LLM inference. **No third-party neede
75
80
 
76
81
  ### Reputation Tiers (based on lifetime USDC paid to BlockRun)
77
82
 
78
- | Tier | Condition | Worker reward | Task priority |
79
- |------|-----------|--------------|---------------|
80
- | **Bronze** | New user | $0.0001/check (1x) | Standard |
81
- | **Silver** | ≥ $10 paid | $0.00012/check (1.2x) | Priority assignment |
82
- | **Gold** | ≥ $50 paid | $0.00015/check (1.5x) | High-value tasks |
83
- | **Platinum** | ≥ $200 paid | $0.0002/check (2x) | Enterprise tasks, first pick |
83
+ | Tier | Condition | Worker reward | Task priority |
84
+ | ------------ | ----------- | --------------------- | ---------------------------- |
85
+ | **Bronze** | New user | $0.0001/check (1x) | Standard |
86
+ | **Silver** | ≥ $10 paid | $0.00012/check (1.2x) | Priority assignment |
87
+ | **Gold** | ≥ $50 paid | $0.00015/check (1.5x) | High-value tasks |
88
+ | **Platinum** | ≥ $200 paid | $0.0002/check (2x) | Enterprise tasks, first pick |
84
89
 
85
90
  Reputation is computed from BlockRun's own GCS logs (LLM call history per wallet), refreshed daily. Cached in DB per wallet — not queried on every request.
86
91
 
@@ -91,6 +96,7 @@ Reputation is computed from BlockRun's own GCS logs (LLM call history per wallet
91
96
  ClawRouter users are developers on their own machines, not 24/7 server operators.
92
97
 
93
98
  **Estimated concurrent online workers:**
99
+
94
100
  ```
95
101
  Peak (US + EU working hours): 200–300
96
102
  Average (any time): 100–150
@@ -144,6 +150,7 @@ Same CDP facilitator `/settle` endpoint. No new payment infrastructure.
144
150
  ### Payout Batching
145
151
 
146
152
  Do NOT pay $0.0001 per check immediately:
153
+
147
154
  - Accumulate credits in DB per worker
148
155
  - Pay when worker reaches **$0.01 threshold** (~100 checks)
149
156
  - Base L2 gas ≈ $0.0001/tx → gas overhead = **1%** of payout
@@ -154,10 +161,10 @@ Do NOT pay $0.0001 per check immediately:
154
161
 
155
162
  Every payout writes to **two places simultaneously**:
156
163
 
157
- | Layer | Purpose | Data |
158
- |-------|---------|------|
159
- | **DB** | Fast reads, operational queries, pending credits | All tables below |
160
- | **Base calldata** | Immutable audit trail, independent verification | Payout receipts only |
164
+ | Layer | Purpose | Data |
165
+ | ----------------- | ------------------------------------------------ | -------------------- |
166
+ | **DB** | Fast reads, operational queries, pending credits | All tables below |
167
+ | **Base calldata** | Immutable audit trail, independent verification | Payout receipts only |
161
168
 
162
169
  ### DB Schema
163
170
 
@@ -239,8 +246,8 @@ A separate 0 ETH transaction broadcast alongside the USDC transfer:
239
246
  Workers are **existing paying ClawRouter users**. The work is trivially cheap:
240
247
 
241
248
  ```javascript
242
- const res = await fetch(url, { signal: AbortSignal.timeout(10000) })
243
- return { status: res.status, latency: Date.now() - start }
249
+ const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
250
+ return { status: res.status, latency: Date.now() - start };
244
251
  ```
245
252
 
246
253
  Cost to do the work: ~10ms, $0.
@@ -255,34 +262,37 @@ Future (V2): nonce injection for BlockRun-owned endpoints, spot-check verificati
255
262
 
256
263
  ## All Design Decisions
257
264
 
258
- | Question | Decision |
259
- |----------|---------|
260
- | 3-worker consensus needed? | No — redundancy only, not verification |
261
- | How to pay workers? | x402 reversed payTo, same CDP facilitator |
262
- | Workers always online? | No — 100-150 avg, 3x redundancy compensates |
263
- | Verify work authenticity? | Trust-based (paying users, no incentive to cheat) |
264
- | Track credits per worker? | DB (primary) + Base calldata (audit) |
265
- | Pay per check on-chain? | No — batch at $0.01 threshold, 1% gas overhead |
266
- | Calldata mechanism? | Separate 0 ETH tx to BLOCKRUN_LOG_ADDRESS |
267
- | Reputation source? | BlockRun's own GCS logs, no third-party |
268
- | DB choice? | TBD — any Postgres-compatible works |
265
+ | Question | Decision |
266
+ | -------------------------- | ------------------------------------------------- |
267
+ | 3-worker consensus needed? | No — redundancy only, not verification |
268
+ | How to pay workers? | x402 reversed payTo, same CDP facilitator |
269
+ | Workers always online? | No — 100-150 avg, 3x redundancy compensates |
270
+ | Verify work authenticity? | Trust-based (paying users, no incentive to cheat) |
271
+ | Track credits per worker? | DB (primary) + Base calldata (audit) |
272
+ | Pay per check on-chain? | No — batch at $0.01 threshold, 1% gas overhead |
273
+ | Calldata mechanism? | Separate 0 ETH tx to BLOCKRUN_LOG_ADDRESS |
274
+ | Reputation source? | BlockRun's own GCS logs, no third-party |
275
+ | DB choice? | TBD — any Postgres-compatible works |
269
276
 
270
277
  ---
271
278
 
272
279
  ## Go-to-Market
273
280
 
274
281
  ### Phase 1: Supply Side (Month 1–2)
282
+
275
283
  - Ship `CLAWROUTER_WORKER=1` to 1,000 existing users
276
284
  - Pilot: 3 hardcoded tasks monitoring BlockRun's own endpoints
277
285
  - Target: 50+ active workers, end-to-end payment verified on-chain
278
286
 
279
287
  ### Phase 2: First Buyers (Month 2–3)
288
+
280
289
  - Buyer dashboard — register any endpoint, choose SLA tier
281
290
  - First 10 customers: 30-day free trial
282
291
  - Publish node map (marketing)
283
292
  - Target: 5 paying customers, $2,500 MRR
284
293
 
285
294
  ### Phase 3: Scale (Month 3–6)
295
+
286
296
  - Standard/Premium tiers with BlockRun-backed SLA
287
297
  - "State of Web3 Uptime" report from aggregated data
288
298
  - Coinbase/Base ecosystem partnership
@@ -292,14 +302,14 @@ Future (V2): nonce injection for BlockRun-owned endpoints, spot-check verificati
292
302
 
293
303
  ## Success Metrics
294
304
 
295
- | Metric | Month 3 | Month 6 |
296
- |--------|---------|---------|
297
- | Active workers | 50 | 200 |
298
- | Monitored endpoints | 25 | 150 |
299
- | Paying customers | 5 | 30 |
300
- | MRR | $2,500 | $15,000 |
301
- | USDC to workers/month | $250 | $1,500 |
302
- | On-chain payout txs | verifiable | verifiable |
305
+ | Metric | Month 3 | Month 6 |
306
+ | --------------------- | ---------- | ---------- |
307
+ | Active workers | 50 | 200 |
308
+ | Monitored endpoints | 25 | 150 |
309
+ | Paying customers | 5 | 30 |
310
+ | MRR | $2,500 | $15,000 |
311
+ | USDC to workers/month | $250 | $1,500 |
312
+ | On-chain payout txs | verifiable | verifiable |
303
313
 
304
314
  ---
305
315
 
@@ -320,26 +330,29 @@ Future (V2): nonce injection for BlockRun-owned endpoints, spot-check verificati
320
330
  ## Files to Touch
321
331
 
322
332
  ### ClawRouter
323
- | File | Action |
324
- |------|--------|
325
- | `src/worker/types.ts` | CREATE |
333
+
334
+ | File | Action |
335
+ | ---------------------- | ------ |
336
+ | `src/worker/types.ts` | CREATE |
326
337
  | `src/worker/checks.ts` | CREATE |
327
- | `src/worker/index.ts` | CREATE |
328
- | `src/index.ts` | MODIFY |
338
+ | `src/worker/index.ts` | CREATE |
339
+ | `src/index.ts` | MODIFY |
329
340
 
330
341
  ### BlockRun
331
- | File | Action |
332
- |------|--------|
333
- | `src/lib/worker-tasks.ts` | CREATE |
334
- | `src/lib/worker-credits.ts` | CREATE |
335
- | `src/lib/worker-payouts.ts` | CREATE |
336
- | `src/lib/worker-reputation.ts` | CREATE |
337
- | `src/app/api/v1/worker/tasks/route.ts` | CREATE |
342
+
343
+ | File | Action |
344
+ | ---------------------------------------- | ------ |
345
+ | `src/lib/worker-tasks.ts` | CREATE |
346
+ | `src/lib/worker-credits.ts` | CREATE |
347
+ | `src/lib/worker-payouts.ts` | CREATE |
348
+ | `src/lib/worker-reputation.ts` | CREATE |
349
+ | `src/app/api/v1/worker/tasks/route.ts` | CREATE |
338
350
  | `src/app/api/v1/worker/results/route.ts` | CREATE |
339
351
 
340
352
  ### Environment Variables
341
353
 
342
354
  **ClawRouter `.env` / shell:**
355
+
343
356
  ```bash
344
357
  CLAWROUTER_WORKER=1
345
358
  WORKER_REGION=US-West # optional
@@ -347,6 +360,7 @@ BLOCKRUN_API_BASE=https://blockrun.ai/api # override for local dev
347
360
  ```
348
361
 
349
362
  **BlockRun `.env.local`:**
363
+
350
364
  ```bash
351
365
  WORKER_PAYOUT_WALLET_KEY=0x... # treasury signing key — never commit
352
366
  BLOCKRUN_LOG_ADDRESS=0x... # BlockRun's own address for calldata logs
@@ -361,37 +375,38 @@ DATABASE_URL=postgres://... # your DB
361
375
 
362
376
  ```typescript
363
377
  export interface WorkerTask {
364
- id: string
365
- type: "http_check"
366
- url: string
367
- expectedStatus: number
368
- timeoutMs: number
369
- rewardMicros: number
370
- region?: string
378
+ id: string;
379
+ type: "http_check";
380
+ url: string;
381
+ expectedStatus: number;
382
+ timeoutMs: number;
383
+ rewardMicros: number;
384
+ region?: string;
371
385
  }
372
386
 
373
387
  export interface WorkerResult {
374
- taskId: string
375
- workerAddress: string
376
- timestamp: number
377
- success: boolean
378
- responseTimeMs: number
379
- statusCode?: number
380
- error?: string
388
+ taskId: string;
389
+ workerAddress: string;
390
+ timestamp: number;
391
+ success: boolean;
392
+ responseTimeMs: number;
393
+ statusCode?: number;
394
+ error?: string;
381
395
  // EIP-191 signature of JSON.stringify({ taskId, workerAddress, timestamp, success })
382
- signature: string
396
+ signature: string;
383
397
  }
384
398
 
385
399
  export interface WorkerStatus {
386
- address: string
387
- completedTasks: number
388
- totalEarnedMicros: number
389
- lastPollAt?: number
390
- busy: boolean
400
+ address: string;
401
+ completedTasks: number;
402
+ totalEarnedMicros: number;
403
+ lastPollAt?: number;
404
+ busy: boolean;
391
405
  }
392
406
  ```
393
407
 
394
408
  **Steps:**
409
+
395
410
  1. `mkdir src/worker && touch src/worker/types.ts` — paste content above
396
411
  2. `npx tsc --noEmit` — expect no errors
397
412
  3. `git add src/worker/types.ts && git commit -m "feat(worker): add types"`
@@ -403,57 +418,60 @@ export interface WorkerStatus {
403
418
  **File:** `src/worker/checks.ts`
404
419
 
405
420
  ```typescript
406
- import type { WorkerTask } from "./types.js"
421
+ import type { WorkerTask } from "./types.js";
407
422
 
408
423
  export async function executeHttpCheck(task: WorkerTask): Promise<{
409
- success: boolean
410
- responseTimeMs: number
411
- statusCode?: number
412
- error?: string
424
+ success: boolean;
425
+ responseTimeMs: number;
426
+ statusCode?: number;
427
+ error?: string;
413
428
  }> {
414
- const start = Date.now()
429
+ const start = Date.now();
415
430
  try {
416
431
  const res = await fetch(task.url, {
417
432
  method: "GET",
418
433
  signal: AbortSignal.timeout(task.timeoutMs),
419
434
  redirect: "follow",
420
435
  headers: { "User-Agent": "BlockRun-Worker/1.0" },
421
- })
436
+ });
422
437
  return {
423
438
  success: res.status === task.expectedStatus,
424
439
  responseTimeMs: Date.now() - start,
425
440
  statusCode: res.status,
426
- }
441
+ };
427
442
  } catch (err) {
428
- const isTimeout = err instanceof Error &&
429
- (err.name === "TimeoutError" || err.name === "AbortError")
443
+ const isTimeout =
444
+ err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
430
445
  return {
431
446
  success: false,
432
447
  responseTimeMs: Date.now() - start,
433
448
  error: isTimeout
434
449
  ? `Timeout after ${task.timeoutMs}ms`
435
- : err instanceof Error ? err.message : String(err),
436
- }
450
+ : err instanceof Error
451
+ ? err.message
452
+ : String(err),
453
+ };
437
454
  }
438
455
  }
439
456
 
440
457
  // Must produce identical JSON on both sides for signature verification
441
458
  export function buildSignableMessage(params: {
442
- taskId: string
443
- workerAddress: string
444
- timestamp: number
445
- success: boolean
459
+ taskId: string;
460
+ workerAddress: string;
461
+ timestamp: number;
462
+ success: boolean;
446
463
  }): string {
447
464
  return JSON.stringify({
448
465
  taskId: params.taskId,
449
466
  workerAddress: params.workerAddress,
450
467
  timestamp: params.timestamp,
451
468
  success: params.success,
452
- })
469
+ });
453
470
  }
454
471
  ```
455
472
 
456
473
  **Steps:**
474
+
457
475
  1. Create `src/worker/checks.ts` — paste above
458
476
  2. `npx tsc --noEmit`
459
477
  3. `git add src/worker/checks.ts && git commit -m "feat(worker): add HTTP check executor"`
@@ -465,93 +483,93 @@ export function buildSignableMessage(params: {
465
483
  **File:** `src/worker/index.ts`
466
484
 
467
485
  ```typescript
468
- import { privateKeyToAccount } from "viem/accounts"
469
- import type { WorkerTask, WorkerResult, WorkerStatus } from "./types.js"
470
- import { executeHttpCheck, buildSignableMessage } from "./checks.js"
486
+ import { privateKeyToAccount } from "viem/accounts";
487
+ import type { WorkerTask, WorkerResult, WorkerStatus } from "./types.js";
488
+ import { executeHttpCheck, buildSignableMessage } from "./checks.js";
471
489
 
472
- const BLOCKRUN_API = process.env.BLOCKRUN_API_BASE ?? "https://blockrun.ai/api"
473
- const POLL_INTERVAL_MS = 30_000
474
- const MAX_CONCURRENT = 10
475
- const REGION = process.env.WORKER_REGION ?? "unknown"
490
+ const BLOCKRUN_API = process.env.BLOCKRUN_API_BASE ?? "https://blockrun.ai/api";
491
+ const POLL_INTERVAL_MS = 30_000;
492
+ const MAX_CONCURRENT = 10;
493
+ const REGION = process.env.WORKER_REGION ?? "unknown";
476
494
 
477
495
  export class WorkerNode {
478
- private privateKey: `0x${string}`
479
- private address: string
480
- private apiBase: string
481
- private busy = false
482
- private status: WorkerStatus
496
+ private privateKey: `0x${string}`;
497
+ private address: string;
498
+ private apiBase: string;
499
+ private busy = false;
500
+ private status: WorkerStatus;
483
501
 
484
502
  constructor(walletKey: string, walletAddress: string, apiBase = BLOCKRUN_API) {
485
- this.privateKey = walletKey as `0x${string}`
486
- this.address = walletAddress
487
- this.apiBase = apiBase
488
- this.status = { address: walletAddress, completedTasks: 0, totalEarnedMicros: 0, busy: false }
503
+ this.privateKey = walletKey as `0x${string}`;
504
+ this.address = walletAddress;
505
+ this.apiBase = apiBase;
506
+ this.status = { address: walletAddress, completedTasks: 0, totalEarnedMicros: 0, busy: false };
489
507
  }
490
508
 
491
509
  startPolling(): void {
492
- console.log(`[Worker] Starting — ${this.address} region=${REGION}`)
493
- this.poll().catch(console.error)
494
- setInterval(() => this.poll().catch(console.error), POLL_INTERVAL_MS)
510
+ console.log(`[Worker] Starting — ${this.address} region=${REGION}`);
511
+ this.poll().catch(console.error);
512
+ setInterval(() => this.poll().catch(console.error), POLL_INTERVAL_MS);
495
513
  }
496
514
 
497
515
  getStatus(): WorkerStatus {
498
- return { ...this.status, busy: this.busy }
516
+ return { ...this.status, busy: this.busy };
499
517
  }
500
518
 
501
519
  private async poll(): Promise<void> {
502
- if (this.busy) return
503
- let tasks: WorkerTask[] = []
520
+ if (this.busy) return;
521
+ let tasks: WorkerTask[] = [];
504
522
  try {
505
- tasks = await this.fetchTasks()
523
+ tasks = await this.fetchTasks();
506
524
  } catch (err) {
507
- console.error(`[Worker] fetch tasks failed:`, err instanceof Error ? err.message : err)
508
- return
525
+ console.error(`[Worker] fetch tasks failed:`, err instanceof Error ? err.message : err);
526
+ return;
509
527
  }
510
- if (tasks.length === 0) return
528
+ if (tasks.length === 0) return;
511
529
 
512
- this.busy = true
513
- this.status.lastPollAt = Date.now()
514
- console.log(`[Worker] Executing ${tasks.length} task(s)`)
530
+ this.busy = true;
531
+ this.status.lastPollAt = Date.now();
532
+ console.log(`[Worker] Executing ${tasks.length} task(s)`);
515
533
  try {
516
- const results = await this.executeBatch(tasks)
517
- await this.submitResults(results)
534
+ const results = await this.executeBatch(tasks);
535
+ await this.submitResults(results);
518
536
  } finally {
519
- this.busy = false
537
+ this.busy = false;
520
538
  }
521
539
  }
522
540
 
523
541
  private async fetchTasks(): Promise<WorkerTask[]> {
524
- const url = `${this.apiBase}/v1/worker/tasks?address=${this.address}&region=${REGION}`
542
+ const url = `${this.apiBase}/v1/worker/tasks?address=${this.address}&region=${REGION}`;
525
543
  const res = await fetch(url, {
526
544
  signal: AbortSignal.timeout(10_000),
527
545
  headers: { "User-Agent": "BlockRun-Worker/1.0" },
528
- })
529
- if (!res.ok) throw new Error(`tasks endpoint ${res.status}`)
530
- return res.json() as Promise<WorkerTask[]>
546
+ });
547
+ if (!res.ok) throw new Error(`tasks endpoint ${res.status}`);
548
+ return res.json() as Promise<WorkerTask[]>;
531
549
  }
532
550
 
533
551
  private async executeBatch(tasks: WorkerTask[]): Promise<WorkerResult[]> {
534
- const results: WorkerResult[] = []
552
+ const results: WorkerResult[] = [];
535
553
  for (let i = 0; i < tasks.length; i += MAX_CONCURRENT) {
536
- const chunk = tasks.slice(i, i + MAX_CONCURRENT)
537
- const done = await Promise.all(chunk.map(t => this.executeAndSign(t)))
538
- results.push(...done)
554
+ const chunk = tasks.slice(i, i + MAX_CONCURRENT);
555
+ const done = await Promise.all(chunk.map((t) => this.executeAndSign(t)));
556
+ results.push(...done);
539
557
  }
540
- return results
558
+ return results;
541
559
  }
542
560
 
543
561
  private async executeAndSign(task: WorkerTask): Promise<WorkerResult> {
544
- const check = await executeHttpCheck(task)
545
- const timestamp = Date.now()
562
+ const check = await executeHttpCheck(task);
563
+ const timestamp = Date.now();
546
564
  const message = buildSignableMessage({
547
565
  taskId: task.id,
548
566
  workerAddress: this.address,
549
567
  timestamp,
550
568
  success: check.success,
551
- })
552
- const account = privateKeyToAccount(this.privateKey)
553
- const signature = await account.signMessage({ message })
554
- return { taskId: task.id, workerAddress: this.address, timestamp, ...check, signature }
569
+ });
570
+ const account = privateKeyToAccount(this.privateKey);
571
+ const signature = await account.signMessage({ message });
572
+ return { taskId: task.id, workerAddress: this.address, timestamp, ...check, signature };
555
573
  }
556
574
 
557
575
  private async submitResults(results: WorkerResult[]): Promise<void> {
@@ -561,19 +579,23 @@ export class WorkerNode {
561
579
  headers: { "Content-Type": "application/json", "User-Agent": "BlockRun-Worker/1.0" },
562
580
  body: JSON.stringify(results),
563
581
  signal: AbortSignal.timeout(15_000),
564
- })
565
- if (!res.ok) { console.error(`[Worker] submit failed ${res.status}`); return }
566
- const data = await res.json() as { accepted: number; earned: string }
567
- this.status.completedTasks += data.accepted
568
- console.log(`[Worker] ✓ ${data.accepted} result(s) accepted, earned $${data.earned} USDC`)
582
+ });
583
+ if (!res.ok) {
584
+ console.error(`[Worker] submit failed ${res.status}`);
585
+ return;
586
+ }
587
+ const data = (await res.json()) as { accepted: number; earned: string };
588
+ this.status.completedTasks += data.accepted;
589
+ console.log(`[Worker] ✓ ${data.accepted} result(s) accepted, earned $${data.earned} USDC`);
569
590
  } catch (err) {
570
- console.error(`[Worker] submit error:`, err instanceof Error ? err.message : err)
591
+ console.error(`[Worker] submit error:`, err instanceof Error ? err.message : err);
571
592
  }
572
593
  }
573
594
  }
574
595
  ```
575
596
 
576
597
  **Steps:**
598
+
577
599
  1. Create `src/worker/index.ts` — paste above
578
600
  2. `npx tsc --noEmit`
579
601
  3. `git add src/worker/index.ts && git commit -m "feat(worker): add WorkerNode class"`
@@ -585,26 +607,27 @@ export class WorkerNode {
585
607
  **File:** `src/index.ts` — modify `startProxyInBackground()`.
586
608
 
587
609
  Find this block (~line 423):
610
+
588
611
  ```typescript
589
612
  setActiveProxy(proxy);
590
613
  activeProxyHandle = proxy;
591
614
  ```
592
615
 
593
616
  Add immediately after:
617
+
594
618
  ```typescript
595
- const workerMode =
596
- process.env.CLAWROUTER_WORKER === "1" ||
597
- process.argv.includes("--worker")
619
+ const workerMode = process.env.CLAWROUTER_WORKER === "1" || process.argv.includes("--worker");
598
620
 
599
621
  if (workerMode) {
600
- const { WorkerNode } = await import("./worker/index.js")
601
- const worker = new WorkerNode(walletKey, address)
602
- worker.startPolling()
603
- api.logger.info(`[Worker] Mode active — polling every 30s, wallet: ${address}`)
622
+ const { WorkerNode } = await import("./worker/index.js");
623
+ const worker = new WorkerNode(walletKey, address);
624
+ worker.startPolling();
625
+ api.logger.info(`[Worker] Mode active — polling every 30s, wallet: ${address}`);
604
626
  }
605
627
  ```
606
628
 
607
629
  **Steps:**
630
+
608
631
  1. Edit `src/index.ts`
609
632
  2. `npx tsc --noEmit`
610
633
  3. `git add src/index.ts && git commit -m "feat(worker): activate WorkerNode on CLAWROUTER_WORKER=1"`
@@ -664,6 +687,7 @@ CREATE TABLE wallet_reputation (
664
687
  ```
665
688
 
666
689
  **Steps:**
690
+
667
691
  1. Run migration against dev DB
668
692
  2. Verify all 4 tables exist
669
693
  3. `git commit -m "feat(worker): add DB migration"`
@@ -676,58 +700,80 @@ CREATE TABLE wallet_reputation (
676
700
 
677
701
  ```typescript
678
702
  export interface WorkerTask {
679
- id: string
680
- type: "http_check"
681
- url: string
682
- expectedStatus: number
683
- timeoutMs: number
684
- rewardMicros: number
685
- region?: string
703
+ id: string;
704
+ type: "http_check";
705
+ url: string;
706
+ expectedStatus: number;
707
+ timeoutMs: number;
708
+ rewardMicros: number;
709
+ region?: string;
686
710
  }
687
711
 
688
712
  export const PILOT_TASKS: WorkerTask[] = [
689
- { id: "task_br_health", type: "http_check", url: "https://blockrun.ai/api/health", expectedStatus: 200, timeoutMs: 10_000, rewardMicros: 100 },
690
- { id: "task_br_models", type: "http_check", url: "https://blockrun.ai/api/v1/models", expectedStatus: 200, timeoutMs: 10_000, rewardMicros: 100 },
691
- { id: "task_base_rpc", type: "http_check", url: "https://mainnet.base.org", expectedStatus: 200, timeoutMs: 10_000, rewardMicros: 150 },
692
- ]
713
+ {
714
+ id: "task_br_health",
715
+ type: "http_check",
716
+ url: "https://blockrun.ai/api/health",
717
+ expectedStatus: 200,
718
+ timeoutMs: 10_000,
719
+ rewardMicros: 100,
720
+ },
721
+ {
722
+ id: "task_br_models",
723
+ type: "http_check",
724
+ url: "https://blockrun.ai/api/v1/models",
725
+ expectedStatus: 200,
726
+ timeoutMs: 10_000,
727
+ rewardMicros: 100,
728
+ },
729
+ {
730
+ id: "task_base_rpc",
731
+ type: "http_check",
732
+ url: "https://mainnet.base.org",
733
+ expectedStatus: 200,
734
+ timeoutMs: 10_000,
735
+ rewardMicros: 150,
736
+ },
737
+ ];
693
738
 
694
739
  // Track recent assignments: taskId → [{ workerAddress, assignedAt }]
695
740
  // Each task assigned to up to 3 workers per cycle. First to submit wins.
696
- const assignments = new Map<string, Array<{ workerAddress: string; assignedAt: number }>>()
697
- const CYCLE_MS = 30_000
698
- const MAX_PER_TASK = 3
741
+ const assignments = new Map<string, Array<{ workerAddress: string; assignedAt: number }>>();
742
+ const CYCLE_MS = 30_000;
743
+ const MAX_PER_TASK = 3;
699
744
 
700
745
  export function getTasksForWorker(workerAddress: string, _region?: string): WorkerTask[] {
701
- const now = Date.now()
702
- const addr = workerAddress.toLowerCase()
703
-
704
- return PILOT_TASKS.filter(task => {
705
- const list = assignments.get(task.id) ?? []
706
- const fresh = list.filter(a => now - a.assignedAt < CYCLE_MS)
707
- assignments.set(task.id, fresh)
708
-
709
- if (fresh.some(a => a.workerAddress === addr)) return false
710
- if (fresh.length >= MAX_PER_TASK) return false
711
- return true
712
- })
746
+ const now = Date.now();
747
+ const addr = workerAddress.toLowerCase();
748
+
749
+ return PILOT_TASKS.filter((task) => {
750
+ const list = assignments.get(task.id) ?? [];
751
+ const fresh = list.filter((a) => now - a.assignedAt < CYCLE_MS);
752
+ assignments.set(task.id, fresh);
753
+
754
+ if (fresh.some((a) => a.workerAddress === addr)) return false;
755
+ if (fresh.length >= MAX_PER_TASK) return false;
756
+ return true;
757
+ });
713
758
  }
714
759
 
715
760
  export function markAssigned(taskIds: string[], workerAddress: string): void {
716
- const addr = workerAddress.toLowerCase()
717
- const now = Date.now()
761
+ const addr = workerAddress.toLowerCase();
762
+ const now = Date.now();
718
763
  for (const id of taskIds) {
719
- const list = assignments.get(id) ?? []
720
- list.push({ workerAddress: addr, assignedAt: now })
721
- assignments.set(id, list)
764
+ const list = assignments.get(id) ?? [];
765
+ list.push({ workerAddress: addr, assignedAt: now });
766
+ assignments.set(id, list);
722
767
  }
723
768
  }
724
769
 
725
770
  export function getTaskById(taskId: string): WorkerTask | undefined {
726
- return PILOT_TASKS.find(t => t.id === taskId)
771
+ return PILOT_TASKS.find((t) => t.id === taskId);
727
772
  }
728
773
  ```
729
774
 
730
775
  **Steps:**
776
+
731
777
  1. Create `src/lib/worker-tasks.ts`
732
778
  2. `npx tsc --noEmit`
733
779
  3. `git commit -m "feat(worker): task registry with 3-worker redundancy"`
@@ -739,20 +785,21 @@ export function getTaskById(taskId: string): WorkerTask | undefined {
739
785
  **File:** `src/lib/worker-credits.ts`
740
786
 
741
787
  ```typescript
742
- import { db } from "@/lib/db" // your DB client — swap for actual import
743
- import { getReputationMultiplier } from "./worker-reputation"
788
+ import { db } from "@/lib/db"; // your DB client — swap for actual import
789
+ import { getReputationMultiplier } from "./worker-reputation";
744
790
 
745
- const PAYOUT_THRESHOLD_MICROS = 10_000 // $0.01
791
+ const PAYOUT_THRESHOLD_MICROS = 10_000; // $0.01
746
792
 
747
793
  export async function creditWorker(
748
794
  workerAddress: string,
749
795
  baseRewardMicros: number,
750
796
  ): Promise<{ pendingMicros: number; thresholdReached: boolean }> {
751
- const multiplier = await getReputationMultiplier(workerAddress)
752
- const earned = Math.floor(baseRewardMicros * multiplier)
797
+ const multiplier = await getReputationMultiplier(workerAddress);
798
+ const earned = Math.floor(baseRewardMicros * multiplier);
753
799
 
754
800
  // Atomic upsert — safe for concurrent Cloud Run instances
755
- const result = await db.query<{ pending_micros: number }>(`
801
+ const result = await db.query<{ pending_micros: number }>(
802
+ `
756
803
  INSERT INTO worker_credits (address, pending_micros, total_earned, updated_at)
757
804
  VALUES ($1, $2, $2, NOW())
758
805
  ON CONFLICT (address) DO UPDATE SET
@@ -760,13 +807,15 @@ export async function creditWorker(
760
807
  total_earned = worker_credits.total_earned + $2,
761
808
  updated_at = NOW()
762
809
  RETURNING pending_micros
763
- `, [workerAddress.toLowerCase(), earned])
810
+ `,
811
+ [workerAddress.toLowerCase(), earned],
812
+ );
764
813
 
765
- const pendingMicros = result.rows[0].pending_micros
814
+ const pendingMicros = result.rows[0].pending_micros;
766
815
  return {
767
816
  pendingMicros,
768
817
  thresholdReached: pendingMicros >= PAYOUT_THRESHOLD_MICROS,
769
- }
818
+ };
770
819
  }
771
820
 
772
821
  export async function resetPendingCredits(
@@ -775,7 +824,8 @@ export async function resetPendingCredits(
775
824
  amountMicros: number,
776
825
  txHash: string,
777
826
  ): Promise<void> {
778
- await db.query(`
827
+ await db.query(
828
+ `
779
829
  UPDATE worker_credits SET
780
830
  pending_micros = pending_micros - $2,
781
831
  total_paid = total_paid + $2,
@@ -783,19 +833,22 @@ export async function resetPendingCredits(
783
833
  last_payout_tx = $3,
784
834
  updated_at = NOW()
785
835
  WHERE address = $1
786
- `, [workerAddress.toLowerCase(), amountMicros, txHash])
836
+ `,
837
+ [workerAddress.toLowerCase(), amountMicros, txHash],
838
+ );
787
839
  }
788
840
 
789
841
  export async function getPendingMicros(workerAddress: string): Promise<number> {
790
842
  const result = await db.query<{ pending_micros: number }>(
791
843
  `SELECT pending_micros FROM worker_credits WHERE address = $1`,
792
- [workerAddress.toLowerCase()]
793
- )
794
- return result.rows[0]?.pending_micros ?? 0
844
+ [workerAddress.toLowerCase()],
845
+ );
846
+ return result.rows[0]?.pending_micros ?? 0;
795
847
  }
796
848
  ```
797
849
 
798
850
  **Steps:**
851
+
799
852
  1. Create `src/lib/worker-credits.ts`
800
853
  2. Wire your actual DB client at `@/lib/db` (swap the import)
801
854
  3. `npx tsc --noEmit`
@@ -808,58 +861,68 @@ export async function getPendingMicros(workerAddress: string): Promise<number> {
808
861
  **File:** `src/lib/worker-reputation.ts`
809
862
 
810
863
  ```typescript
811
- import { db } from "@/lib/db"
864
+ import { db } from "@/lib/db";
812
865
 
813
866
  const TIERS = [
814
867
  { tier: "platinum", minPaid: 200, multiplier: 2.0 },
815
- { tier: "gold", minPaid: 50, multiplier: 1.5 },
816
- { tier: "silver", minPaid: 10, multiplier: 1.2 },
817
- { tier: "bronze", minPaid: 0, multiplier: 1.0 },
818
- ] as const
868
+ { tier: "gold", minPaid: 50, multiplier: 1.5 },
869
+ { tier: "silver", minPaid: 10, multiplier: 1.2 },
870
+ { tier: "bronze", minPaid: 0, multiplier: 1.0 },
871
+ ] as const;
819
872
 
820
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // refresh daily
873
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // refresh daily
821
874
 
822
875
  export async function getReputationMultiplier(workerAddress: string): Promise<number> {
823
- const rep = await getReputation(workerAddress)
824
- return rep.multiplier
876
+ const rep = await getReputation(workerAddress);
877
+ return rep.multiplier;
825
878
  }
826
879
 
827
880
  export async function getReputation(workerAddress: string): Promise<{
828
- tier: string
829
- totalPaidUsd: number
830
- multiplier: number
881
+ tier: string;
882
+ totalPaidUsd: number;
883
+ multiplier: number;
831
884
  }> {
832
- const addr = workerAddress.toLowerCase()
885
+ const addr = workerAddress.toLowerCase();
833
886
 
834
887
  const cached = await db.query<{
835
- tier: string; total_paid_usd: number; multiplier: number; refreshed_at: Date
888
+ tier: string;
889
+ total_paid_usd: number;
890
+ multiplier: number;
891
+ refreshed_at: Date;
836
892
  }>(
837
893
  `SELECT tier, total_paid_usd, multiplier, refreshed_at
838
894
  FROM wallet_reputation WHERE address = $1`,
839
- [addr]
840
- )
895
+ [addr],
896
+ );
841
897
 
842
898
  if (cached.rows.length > 0) {
843
- const row = cached.rows[0]
844
- const age = Date.now() - new Date(row.refreshed_at).getTime()
899
+ const row = cached.rows[0];
900
+ const age = Date.now() - new Date(row.refreshed_at).getTime();
845
901
  if (age < CACHE_TTL_MS) {
846
- return { tier: row.tier, totalPaidUsd: Number(row.total_paid_usd), multiplier: Number(row.multiplier) }
902
+ return {
903
+ tier: row.tier,
904
+ totalPaidUsd: Number(row.total_paid_usd),
905
+ multiplier: Number(row.multiplier),
906
+ };
847
907
  }
848
908
  }
849
909
 
850
910
  // Cache miss or stale — recompute from GCS logs
851
911
  // NOTE: In production this should be a background job, not per-request
852
- const totalPaidUsd = await computeTotalPaidFromGCS(addr)
853
- const tierEntry = TIERS.find(t => totalPaidUsd >= t.minPaid) ?? TIERS[TIERS.length - 1]
912
+ const totalPaidUsd = await computeTotalPaidFromGCS(addr);
913
+ const tierEntry = TIERS.find((t) => totalPaidUsd >= t.minPaid) ?? TIERS[TIERS.length - 1];
854
914
 
855
- await db.query(`
915
+ await db.query(
916
+ `
856
917
  INSERT INTO wallet_reputation (address, total_paid_usd, tier, multiplier, refreshed_at)
857
918
  VALUES ($1, $2, $3, $4, NOW())
858
919
  ON CONFLICT (address) DO UPDATE SET
859
920
  total_paid_usd = $2, tier = $3, multiplier = $4, refreshed_at = NOW()
860
- `, [addr, totalPaidUsd, tierEntry.tier, tierEntry.multiplier])
921
+ `,
922
+ [addr, totalPaidUsd, tierEntry.tier, tierEntry.multiplier],
923
+ );
861
924
 
862
- return { tier: tierEntry.tier, totalPaidUsd, multiplier: tierEntry.multiplier }
925
+ return { tier: tierEntry.tier, totalPaidUsd, multiplier: tierEntry.multiplier };
863
926
  }
864
927
 
865
928
  // Aggregate total USDC paid by this wallet from GCS LLM call logs
@@ -868,11 +931,12 @@ async function computeTotalPaidFromGCS(walletAddress: string): Promise<number> {
868
931
  // TODO (V2): read gs://blockrun-prod-2026-logs/llm-calls/YYYY-MM-DD.jsonl
869
932
  // Filter by wallet, sum cost field
870
933
  // Pilot: return 0 (all workers start at Bronze)
871
- return 0
934
+ return 0;
872
935
  }
873
936
  ```
874
937
 
875
938
  **Steps:**
939
+
876
940
  1. Create `src/lib/worker-reputation.ts`
877
941
  2. `computeTotalPaidFromGCS` is a stub for pilot — implement GCS aggregation in V2
878
942
  3. `git commit -m "feat(worker): reputation module with daily cache"`
@@ -884,44 +948,47 @@ async function computeTotalPaidFromGCS(walletAddress: string): Promise<number> {
884
948
  **File:** `src/lib/worker-payouts.ts`
885
949
 
886
950
  ```typescript
887
- import { createHash } from "crypto"
888
- import { createWalletClient, http } from "viem"
889
- import { base, baseSepolia } from "viem/chains"
890
- import { privateKeyToAccount, signTypedData } from "viem/accounts"
891
- import { getCurrentNetworkConfig } from "./network-config"
892
- import { settlePaymentWithRetry } from "./x402"
893
- import { db } from "@/lib/db"
894
- import { resetPendingCredits } from "./worker-credits"
951
+ import { createHash } from "crypto";
952
+ import { createWalletClient, http } from "viem";
953
+ import { base, baseSepolia } from "viem/chains";
954
+ import { privateKeyToAccount, signTypedData } from "viem/accounts";
955
+ import { getCurrentNetworkConfig } from "./network-config";
956
+ import { settlePaymentWithRetry } from "./x402";
957
+ import { db } from "@/lib/db";
958
+ import { resetPendingCredits } from "./worker-credits";
895
959
 
896
- const PAYOUT_THRESHOLD_MICROS = 10_000
960
+ const PAYOUT_THRESHOLD_MICROS = 10_000;
897
961
 
898
962
  export async function tryPayout(
899
963
  workerAddress: string,
900
964
  pendingMicros: number,
901
965
  resultIds: string[],
902
966
  ): Promise<{ paid: boolean; txHash?: string; logTxHash?: string }> {
903
- if (pendingMicros < PAYOUT_THRESHOLD_MICROS) return { paid: false }
967
+ if (pendingMicros < PAYOUT_THRESHOLD_MICROS) return { paid: false };
904
968
 
905
- const payoutKey = process.env.WORKER_PAYOUT_WALLET_KEY
906
- if (!payoutKey?.startsWith("0x")) throw new Error("WORKER_PAYOUT_WALLET_KEY not set")
969
+ const payoutKey = process.env.WORKER_PAYOUT_WALLET_KEY;
970
+ if (!payoutKey?.startsWith("0x")) throw new Error("WORKER_PAYOUT_WALLET_KEY not set");
907
971
 
908
- const networkConfig = getCurrentNetworkConfig()
909
- const payoutAccount = privateKeyToAccount(payoutKey as `0x${string}`)
972
+ const networkConfig = getCurrentNetworkConfig();
973
+ const payoutAccount = privateKeyToAccount(payoutKey as `0x${string}`);
910
974
 
911
975
  // 1. Insert payout record (pending)
912
- const resultsHash = createHash("sha256").update(resultIds.sort().join(",")).digest("hex")
913
- const payoutResult = await db.query<{ id: string }>(`
976
+ const resultsHash = createHash("sha256").update(resultIds.sort().join(",")).digest("hex");
977
+ const payoutResult = await db.query<{ id: string }>(
978
+ `
914
979
  INSERT INTO worker_payouts (worker_address, amount_micros, result_count, results_hash, status)
915
980
  VALUES ($1, $2, $3, $4, 'pending')
916
981
  RETURNING id
917
- `, [workerAddress.toLowerCase(), pendingMicros, resultIds.length, resultsHash])
918
- const payoutId = payoutResult.rows[0].id
982
+ `,
983
+ [workerAddress.toLowerCase(), pendingMicros, resultIds.length, resultsHash],
984
+ );
985
+ const payoutId = payoutResult.rows[0].id;
919
986
 
920
987
  // 2. Sign TransferWithAuthorization: BlockRun treasury → worker
921
- const validAfter = BigInt(0)
922
- const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600)
923
- const nonceBytes = crypto.getRandomValues(new Uint8Array(32))
924
- const nonce = `0x${Buffer.from(nonceBytes).toString("hex")}` as `0x${string}`
988
+ const validAfter = BigInt(0);
989
+ const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600);
990
+ const nonceBytes = crypto.getRandomValues(new Uint8Array(32));
991
+ const nonce = `0x${Buffer.from(nonceBytes).toString("hex")}` as `0x${string}`;
925
992
 
926
993
  const signature = await signTypedData({
927
994
  privateKey: payoutKey as `0x${string}`,
@@ -933,12 +1000,12 @@ export async function tryPayout(
933
1000
  },
934
1001
  types: {
935
1002
  TransferWithAuthorization: [
936
- { name: "from", type: "address" },
937
- { name: "to", type: "address" },
938
- { name: "value", type: "uint256" },
939
- { name: "validAfter", type: "uint256" },
1003
+ { name: "from", type: "address" },
1004
+ { name: "to", type: "address" },
1005
+ { name: "value", type: "uint256" },
1006
+ { name: "validAfter", type: "uint256" },
940
1007
  { name: "validBefore", type: "uint256" },
941
- { name: "nonce", type: "bytes32" },
1008
+ { name: "nonce", type: "bytes32" },
942
1009
  ],
943
1010
  },
944
1011
  primaryType: "TransferWithAuthorization",
@@ -950,7 +1017,7 @@ export async function tryPayout(
950
1017
  validBefore,
951
1018
  nonce,
952
1019
  },
953
- })
1020
+ });
954
1021
 
955
1022
  // 3. Build x402 payment payload (same format as incoming payments)
956
1023
  const paymentPayload = {
@@ -968,8 +1035,8 @@ export async function tryPayout(
968
1035
  nonce,
969
1036
  },
970
1037
  },
971
- }
972
- const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64")
1038
+ };
1039
+ const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
973
1040
 
974
1041
  const requirements = {
975
1042
  scheme: "exact",
@@ -983,81 +1050,89 @@ export async function tryPayout(
983
1050
  asset: networkConfig.usdc,
984
1051
  outputSchema: null,
985
1052
  extra: null,
986
- }
1053
+ };
987
1054
 
988
1055
  // 4. Settle USDC transfer via CDP facilitator
989
- const settled = await settlePaymentWithRetry(paymentHeader, requirements as never)
990
- if (!settled.success) throw new Error(`Settlement failed: ${settled.error}`)
1056
+ const settled = await settlePaymentWithRetry(paymentHeader, requirements as never);
1057
+ if (!settled.success) throw new Error(`Settlement failed: ${settled.error}`);
991
1058
 
992
- const txHash = settled.txHash!
1059
+ const txHash = settled.txHash!;
993
1060
 
994
1061
  // 5. Write calldata log tx to Base (audit trail — failure does NOT block payout)
995
1062
  const logTxHash = await writeCalldataLog({
996
- workerAddress, amountMicros: pendingMicros,
997
- resultCount: resultIds.length, resultsHash, payoutId,
998
- payoutTxHash: txHash, payoutKey, networkConfig,
999
- })
1063
+ workerAddress,
1064
+ amountMicros: pendingMicros,
1065
+ resultCount: resultIds.length,
1066
+ resultsHash,
1067
+ payoutId,
1068
+ payoutTxHash: txHash,
1069
+ payoutKey,
1070
+ networkConfig,
1071
+ });
1000
1072
 
1001
1073
  // 6. Update DB: mark payout confirmed, reset credits, mark results paid
1002
1074
  await Promise.all([
1003
1075
  db.query(
1004
1076
  `UPDATE worker_payouts SET status='confirmed', tx_hash=$1, log_tx_hash=$2 WHERE id=$3`,
1005
- [txHash, logTxHash, payoutId]
1077
+ [txHash, logTxHash, payoutId],
1006
1078
  ),
1007
1079
  resetPendingCredits(workerAddress, payoutId, pendingMicros, txHash),
1008
- db.query(
1009
- `UPDATE worker_results SET paid=TRUE, payout_id=$1 WHERE id = ANY($2::uuid[])`,
1010
- [payoutId, resultIds]
1011
- ),
1012
- ])
1080
+ db.query(`UPDATE worker_results SET paid=TRUE, payout_id=$1 WHERE id = ANY($2::uuid[])`, [
1081
+ payoutId,
1082
+ resultIds,
1083
+ ]),
1084
+ ]);
1013
1085
 
1014
- return { paid: true, txHash, logTxHash }
1086
+ return { paid: true, txHash, logTxHash };
1015
1087
  }
1016
1088
 
1017
1089
  async function writeCalldataLog(params: {
1018
- workerAddress: string
1019
- amountMicros: number
1020
- resultCount: number
1021
- resultsHash: string
1022
- payoutId: string
1023
- payoutTxHash: string
1024
- payoutKey: string
1025
- networkConfig: ReturnType<typeof getCurrentNetworkConfig>
1090
+ workerAddress: string;
1091
+ amountMicros: number;
1092
+ resultCount: number;
1093
+ resultsHash: string;
1094
+ payoutId: string;
1095
+ payoutTxHash: string;
1096
+ payoutKey: string;
1097
+ networkConfig: ReturnType<typeof getCurrentNetworkConfig>;
1026
1098
  }): Promise<string | undefined> {
1027
- const logAddress = process.env.BLOCKRUN_LOG_ADDRESS as `0x${string}` | undefined
1028
- if (!logAddress) return undefined
1099
+ const logAddress = process.env.BLOCKRUN_LOG_ADDRESS as `0x${string}` | undefined;
1100
+ if (!logAddress) return undefined;
1029
1101
 
1030
1102
  try {
1031
- const chain = params.networkConfig.network === "eip155:8453" ? base : baseSepolia
1032
- const account = privateKeyToAccount(params.payoutKey as `0x${string}`)
1033
-
1034
- const walletClient = createWalletClient({ account, chain, transport: http() })
1035
-
1036
- const data = Buffer.from(JSON.stringify({
1037
- v: 1,
1038
- type: "worker_payout",
1039
- worker: params.workerAddress,
1040
- amountMicros: params.amountMicros,
1041
- resultCount: params.resultCount,
1042
- resultsHash: params.resultsHash,
1043
- payoutId: params.payoutId,
1044
- payoutTxHash: params.payoutTxHash,
1045
- ts: Date.now(),
1046
- })).toString("hex")
1103
+ const chain = params.networkConfig.network === "eip155:8453" ? base : baseSepolia;
1104
+ const account = privateKeyToAccount(params.payoutKey as `0x${string}`);
1105
+
1106
+ const walletClient = createWalletClient({ account, chain, transport: http() });
1107
+
1108
+ const data = Buffer.from(
1109
+ JSON.stringify({
1110
+ v: 1,
1111
+ type: "worker_payout",
1112
+ worker: params.workerAddress,
1113
+ amountMicros: params.amountMicros,
1114
+ resultCount: params.resultCount,
1115
+ resultsHash: params.resultsHash,
1116
+ payoutId: params.payoutId,
1117
+ payoutTxHash: params.payoutTxHash,
1118
+ ts: Date.now(),
1119
+ }),
1120
+ ).toString("hex");
1047
1121
 
1048
1122
  return await walletClient.sendTransaction({
1049
1123
  to: logAddress,
1050
1124
  value: BigInt(0),
1051
1125
  data: `0x${data}` as `0x${string}`,
1052
- })
1126
+ });
1053
1127
  } catch (err) {
1054
- console.error("[Worker Payout] calldata log tx failed:", err)
1055
- return undefined
1128
+ console.error("[Worker Payout] calldata log tx failed:", err);
1129
+ return undefined;
1056
1130
  }
1057
1131
  }
1058
1132
  ```
1059
1133
 
1060
1134
  **Steps:**
1135
+
1061
1136
  1. Create `src/lib/worker-payouts.ts`
1062
1137
  2. Check `network-config.ts` — add `chainId: 8453 / 84532` if missing
1063
1138
  3. `npx tsc --noEmit`
@@ -1070,28 +1145,32 @@ async function writeCalldataLog(params: {
1070
1145
  **File:** `src/app/api/v1/worker/tasks/route.ts`
1071
1146
 
1072
1147
  ```typescript
1073
- import { NextRequest, NextResponse } from "next/server"
1074
- import { getTasksForWorker, markAssigned } from "@/lib/worker-tasks"
1148
+ import { NextRequest, NextResponse } from "next/server";
1149
+ import { getTasksForWorker, markAssigned } from "@/lib/worker-tasks";
1075
1150
 
1076
- export const runtime = "nodejs"
1151
+ export const runtime = "nodejs";
1077
1152
 
1078
1153
  export async function GET(request: NextRequest) {
1079
- const { searchParams } = new URL(request.url)
1080
- const address = searchParams.get("address")
1081
- const region = searchParams.get("region") ?? undefined
1154
+ const { searchParams } = new URL(request.url);
1155
+ const address = searchParams.get("address");
1156
+ const region = searchParams.get("region") ?? undefined;
1082
1157
 
1083
1158
  if (!address?.startsWith("0x")) {
1084
- return NextResponse.json({ error: "address required" }, { status: 400 })
1159
+ return NextResponse.json({ error: "address required" }, { status: 400 });
1085
1160
  }
1086
1161
 
1087
- const tasks = getTasksForWorker(address, region)
1088
- markAssigned(tasks.map(t => t.id), address)
1162
+ const tasks = getTasksForWorker(address, region);
1163
+ markAssigned(
1164
+ tasks.map((t) => t.id),
1165
+ address,
1166
+ );
1089
1167
 
1090
- return NextResponse.json(tasks)
1168
+ return NextResponse.json(tasks);
1091
1169
  }
1092
1170
  ```
1093
1171
 
1094
1172
  **Steps:**
1173
+
1095
1174
  1. `mkdir -p src/app/api/v1/worker/tasks && touch route.ts`
1096
1175
  2. Test: `curl "http://localhost:3000/api/v1/worker/tasks?address=0x000..."`
1097
1176
  3. Expect: JSON array with up to 3 pilot tasks
@@ -1104,47 +1183,50 @@ export async function GET(request: NextRequest) {
1104
1183
  **File:** `src/app/api/v1/worker/results/route.ts`
1105
1184
 
1106
1185
  ```typescript
1107
- import { NextRequest, NextResponse } from "next/server"
1108
- import { recoverMessageAddress } from "viem"
1109
- import { getTaskById } from "@/lib/worker-tasks"
1110
- import { creditWorker } from "@/lib/worker-credits"
1111
- import { tryPayout } from "@/lib/worker-payouts"
1112
- import { db } from "@/lib/db"
1186
+ import { NextRequest, NextResponse } from "next/server";
1187
+ import { recoverMessageAddress } from "viem";
1188
+ import { getTaskById } from "@/lib/worker-tasks";
1189
+ import { creditWorker } from "@/lib/worker-credits";
1190
+ import { tryPayout } from "@/lib/worker-payouts";
1191
+ import { db } from "@/lib/db";
1113
1192
 
1114
- export const runtime = "nodejs"
1193
+ export const runtime = "nodejs";
1115
1194
 
1116
1195
  interface WorkerResult {
1117
- taskId: string
1118
- workerAddress: string
1119
- timestamp: number
1120
- success: boolean
1121
- responseTimeMs: number
1122
- statusCode?: number
1123
- error?: string
1124
- signature: string
1196
+ taskId: string;
1197
+ workerAddress: string;
1198
+ timestamp: number;
1199
+ success: boolean;
1200
+ responseTimeMs: number;
1201
+ statusCode?: number;
1202
+ error?: string;
1203
+ signature: string;
1125
1204
  }
1126
1205
 
1127
1206
  export async function POST(request: NextRequest) {
1128
- let results: WorkerResult[]
1207
+ let results: WorkerResult[];
1129
1208
  try {
1130
- results = await request.json()
1209
+ results = await request.json();
1131
1210
  } catch {
1132
- return NextResponse.json({ error: "invalid JSON" }, { status: 400 })
1211
+ return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
1133
1212
  }
1134
1213
 
1135
1214
  if (!Array.isArray(results) || results.length === 0 || results.length > 50) {
1136
- return NextResponse.json({ error: "results must be array of 1–50" }, { status: 400 })
1215
+ return NextResponse.json({ error: "results must be array of 1–50" }, { status: 400 });
1137
1216
  }
1138
1217
 
1139
- let accepted = 0
1140
- let totalEarnedMicros = 0
1141
- const errors: string[] = []
1142
- const acceptedResultIds: string[] = []
1218
+ let accepted = 0;
1219
+ let totalEarnedMicros = 0;
1220
+ const errors: string[] = [];
1221
+ const acceptedResultIds: string[] = [];
1143
1222
 
1144
1223
  for (const result of results) {
1145
1224
  try {
1146
- const task = getTaskById(result.taskId)
1147
- if (!task) { errors.push(`${result.taskId}: unknown task`); continue }
1225
+ const task = getTaskById(result.taskId);
1226
+ if (!task) {
1227
+ errors.push(`${result.taskId}: unknown task`);
1228
+ continue;
1229
+ }
1148
1230
 
1149
1231
  // Verify EIP-191 signature
1150
1232
  const message = JSON.stringify({
@@ -1152,51 +1234,59 @@ export async function POST(request: NextRequest) {
1152
1234
  workerAddress: result.workerAddress,
1153
1235
  timestamp: result.timestamp,
1154
1236
  success: result.success,
1155
- })
1237
+ });
1156
1238
  const recovered = await recoverMessageAddress({
1157
1239
  message,
1158
1240
  signature: result.signature as `0x${string}`,
1159
- })
1241
+ });
1160
1242
  if (recovered.toLowerCase() !== result.workerAddress.toLowerCase()) {
1161
- errors.push(`${result.taskId}: invalid signature`); continue
1243
+ errors.push(`${result.taskId}: invalid signature`);
1244
+ continue;
1162
1245
  }
1163
1246
 
1164
1247
  // Freshness check (5 min max)
1165
1248
  if (Date.now() - result.timestamp > 5 * 60 * 1000) {
1166
- errors.push(`${result.taskId}: result too old`); continue
1249
+ errors.push(`${result.taskId}: result too old`);
1250
+ continue;
1167
1251
  }
1168
1252
 
1169
1253
  // Write result to DB
1170
- const insertResult = await db.query<{ id: string }>(`
1254
+ const insertResult = await db.query<{ id: string }>(
1255
+ `
1171
1256
  INSERT INTO worker_results
1172
1257
  (task_id, worker_address, timestamp, success, response_time_ms, status_code, reward_micros)
1173
1258
  VALUES ($1, $2, $3, $4, $5, $6, $7)
1174
1259
  RETURNING id
1175
- `, [
1176
- result.taskId,
1177
- result.workerAddress.toLowerCase(),
1178
- result.timestamp,
1179
- result.success,
1180
- result.responseTimeMs,
1181
- result.statusCode ?? null,
1182
- task.rewardMicros,
1183
- ])
1184
- acceptedResultIds.push(insertResult.rows[0].id)
1260
+ `,
1261
+ [
1262
+ result.taskId,
1263
+ result.workerAddress.toLowerCase(),
1264
+ result.timestamp,
1265
+ result.success,
1266
+ result.responseTimeMs,
1267
+ result.statusCode ?? null,
1268
+ task.rewardMicros,
1269
+ ],
1270
+ );
1271
+ acceptedResultIds.push(insertResult.rows[0].id);
1185
1272
 
1186
1273
  // Credit worker (with reputation multiplier)
1187
- const { pendingMicros, thresholdReached } = await creditWorker(result.workerAddress, task.rewardMicros)
1274
+ const { pendingMicros, thresholdReached } = await creditWorker(
1275
+ result.workerAddress,
1276
+ task.rewardMicros,
1277
+ );
1188
1278
 
1189
1279
  // Trigger payout if threshold reached (fire and forget)
1190
1280
  if (thresholdReached) {
1191
- tryPayout(result.workerAddress, pendingMicros, acceptedResultIds)
1192
- .catch(err => console.error(`[Worker Payout] failed for ${result.workerAddress}:`, err))
1281
+ tryPayout(result.workerAddress, pendingMicros, acceptedResultIds).catch((err) =>
1282
+ console.error(`[Worker Payout] failed for ${result.workerAddress}:`, err),
1283
+ );
1193
1284
  }
1194
1285
 
1195
- accepted++
1196
- totalEarnedMicros += task.rewardMicros
1197
-
1286
+ accepted++;
1287
+ totalEarnedMicros += task.rewardMicros;
1198
1288
  } catch (err) {
1199
- errors.push(`${result.taskId}: ${err instanceof Error ? err.message : String(err)}`)
1289
+ errors.push(`${result.taskId}: ${err instanceof Error ? err.message : String(err)}`);
1200
1290
  }
1201
1291
  }
1202
1292
 
@@ -1204,11 +1294,12 @@ export async function POST(request: NextRequest) {
1204
1294
  accepted,
1205
1295
  earned: (totalEarnedMicros / 1_000_000).toFixed(6),
1206
1296
  errors: errors.length > 0 ? errors : undefined,
1207
- })
1297
+ });
1208
1298
  }
1209
1299
  ```
1210
1300
 
1211
1301
  **Steps:**
1302
+
1212
1303
  1. `mkdir -p src/app/api/v1/worker/results && touch route.ts`
1213
1304
  2. `npx tsc --noEmit`
1214
1305
  3. `git commit -m "feat(worker): POST /api/v1/worker/results with sig verify, DB write, payout trigger"`