@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.
Files changed (31) hide show
  1. package/docs/anthropic-cost-savings.md +349 -0
  2. package/docs/architecture.md +559 -0
  3. package/docs/assets/blockrun-248-day-cost-overrun-problem.png +0 -0
  4. package/docs/assets/blockrun-clawrouter-7-layer-token-compression-openclaw.png +0 -0
  5. package/docs/assets/blockrun-clawrouter-observation-compression-97-percent-token-savings.png +0 -0
  6. package/docs/assets/blockrun-clawrouter-openclaw-agentic-proxy-architecture.png +0 -0
  7. package/docs/assets/blockrun-clawrouter-openclaw-automatic-tier-routing-model-selection.png +0 -0
  8. package/docs/assets/blockrun-clawrouter-openclaw-error-classification-retry-storm-prevention.png +0 -0
  9. package/docs/assets/blockrun-clawrouter-openclaw-session-memory-journaling-vs-context-compounding.png +0 -0
  10. package/docs/assets/blockrun-clawrouter-vs-openclaw-standalone-comparison-production-safety.png +0 -0
  11. package/docs/assets/blockrun-clawrouter-x402-usdc-micropayment-wallet-budget-control.png +0 -0
  12. package/docs/assets/blockrun-openclaw-inference-layer-blind-spots.png +0 -0
  13. package/docs/blog-benchmark-2026-03.md +184 -0
  14. package/docs/blog-openclaw-cost-overruns.md +197 -0
  15. package/docs/clawrouter-savings.png +0 -0
  16. package/docs/configuration.md +512 -0
  17. package/docs/features.md +257 -0
  18. package/docs/image-generation.md +380 -0
  19. package/docs/plans/2026-02-03-smart-routing-design.md +267 -0
  20. package/docs/plans/2026-02-13-e2e-docker-deployment.md +1260 -0
  21. package/docs/plans/2026-02-28-worker-network.md +947 -0
  22. package/docs/plans/2026-03-18-error-classification.md +574 -0
  23. package/docs/plans/2026-03-19-exclude-models.md +538 -0
  24. package/docs/routing-profiles.md +81 -0
  25. package/docs/subscription-failover.md +320 -0
  26. package/docs/technical-routing-2026-03.md +322 -0
  27. package/docs/troubleshooting.md +159 -0
  28. package/docs/vision.md +49 -0
  29. package/docs/vs-openrouter.md +157 -0
  30. package/docs/worker-network.md +1241 -0
  31. package/package.json +2 -1
@@ -0,0 +1,1241 @@
1
+ # BlockRun Worker Network
2
+
3
+ > **For Claude implementing this:** Use `superpowers:executing-plans` to implement the tasks section task-by-task.
4
+
5
+ **Goal:** Let ClawRouter users opt in as worker nodes — poll tasks, execute HTTP checks, earn USDC via x402 micropayments.
6
+
7
+ **Architecture:** ClawRouter polls every 30s, signs results with existing wallet key. BlockRun verifies signature, writes to DB, triggers batched x402 payout at $0.01 threshold, simultaneously writes calldata log tx to Base for immutable audit trail.
8
+
9
+ **Tech Stack:** viem (signing + calldata tx), x402 reversed payTo (worker payout), DB (credits ledger), GCS (result logs + reputation source), Base calldata (audit trail)
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ ClawRouter Worker Mode transforms any ClawRouter installation into a node in a decentralized uptime monitoring network. Workers earn USDC by executing HTTP health checks assigned by BlockRun. Buyers purchase monitoring with tamper-proof, multi-node uptime proof — a stronger signal than self-reported metrics.
16
+
17
+ **Current supply-side advantage:** ~1,000 paying ClawRouter users already have wallets and geographic distribution. Turning them into workers requires zero additional setup.
18
+
19
+ ---
20
+
21
+ ## Target Customers
22
+
23
+ ### Primary: Web3 Protocols (Phase 1)
24
+ Blockchain protocols, L1/L2 chains, DeFi applications, RPC providers.
25
+
26
+ **Why they buy:**
27
+ - CEX listing requirements mandate uptime SLA proof
28
+ - Institutional investors require auditable availability records
29
+ - Decentralized proof (multi-node, on-chain payment trail) is more credible than self-reported metrics
30
+ - Already comfortable with USDC payments — no payment education needed
31
+
32
+ **Example customers:** New L2 chains seeking Binance/Coinbase listing, DeFi protocols pitching institutional LPs, bridge protocols, oracle networks
33
+
34
+ ### Secondary: AI API Providers (Phase 2)
35
+ OpenAI, Anthropic, and the long tail of AI API businesses.
36
+
37
+ ### Tertiary: SaaS & Fintech (Phase 2+)
38
+ Any B2B company that sells to enterprises or operates under financial regulation.
39
+
40
+ ---
41
+
42
+ ## Pricing Model
43
+
44
+ ### For Buyers
45
+
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% |
51
+
52
+ Monthly equivalent per endpoint (30s Standard):
53
+ - 2,880 checks/day × 30 × $0.001 = **$86.40/month**
54
+ - Worker cost: 2,880 × $0.0001 = **$8.64/month**
55
+ - **BlockRun margin: $77.76/endpoint/month**
56
+
57
+ ### For Workers
58
+
59
+ Base rate: **$0.0001/check** (100 USDC micros)
60
+
61
+ Multiplied by reputation tier (see below). Payouts trigger at **$0.01 threshold** to minimize gas.
62
+
63
+ ---
64
+
65
+ ## Reputation Flywheel
66
+
67
+ BlockRun already has all payment data from LLM inference. **No third-party needed.**
68
+
69
+ ```
70
+ 用户付钱买 LLM → 积累 reputation
71
+ 高 reputation → 拿到更多/更好 worker 任务
72
+ 赚到更多 USDC → 继续买 LLM
73
+ → 循环
74
+ ```
75
+
76
+ ### Reputation Tiers (based on lifetime USDC paid to BlockRun)
77
+
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 |
84
+
85
+ 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
+
87
+ ---
88
+
89
+ ## Worker Availability Reality
90
+
91
+ ClawRouter users are developers on their own machines, not 24/7 server operators.
92
+
93
+ **Estimated concurrent online workers:**
94
+ ```
95
+ Peak (US + EU working hours): 200–300
96
+ Average (any time): 100–150
97
+ Off-peak (US overnight): 30–50
98
+ ```
99
+
100
+ ### Task Redundancy (not consensus)
101
+
102
+ **Each task is assigned to 3 workers per cycle.** First valid submission wins and gets paid. The other 2 are discarded. This is purely for redundancy — not to verify each other's work. Workers have no incentive to cheat (work is trivially cheap, reward is tiny).
103
+
104
+ ```
105
+ task_br_health sent to:
106
+ worker_042 (US-West) → submits 200, 45ms ✅ WINS, gets paid
107
+ worker_731 (EU) → submits 200, 120ms → discarded
108
+ worker_209 (US-East) → submits 200, 52ms → discarded
109
+ ```
110
+
111
+ **Task queue logic:** Return tasks where `now - lastSuccessfulCheck > targetInterval`. Workers naturally fill gaps. No orphaned assignments.
112
+
113
+ **Standard/Premium tiers:** BlockRun runs always-on backup workers to guarantee baseline coverage.
114
+
115
+ ---
116
+
117
+ ## Payment Architecture
118
+
119
+ ### Full Money Flow
120
+
121
+ ```
122
+ Buyer wallet
123
+ ──$0.001/check──▶ BlockRun (x402, payTo = BlockRun address)
124
+
125
+ DB: worker_credits[address] += rewardMicros
126
+ ↓ (when credits ≥ $0.01)
127
+ BlockRun treasury
128
+ ──$0.01──▶ Worker wallet
129
+ x402 (payTo = worker address)
130
+ + 0 ETH calldata log tx on Base
131
+
132
+ BlockRun keeps the spread ($0.009 per $0.01 payout)
133
+ ```
134
+
135
+ ### Why x402 Both Directions
136
+
137
+ x402 is EIP-3009 TransferWithAuthorization. The `payTo` field is just an address — change it to the worker's wallet:
138
+
139
+ - **Buyer → BlockRun:** `from: buyer, to: blockrunWallet`
140
+ - **BlockRun → Worker:** `from: treasury, to: workerWallet`
141
+
142
+ Same CDP facilitator `/settle` endpoint. No new payment infrastructure.
143
+
144
+ ### Payout Batching
145
+
146
+ Do NOT pay $0.0001 per check immediately:
147
+ - Accumulate credits in DB per worker
148
+ - Pay when worker reaches **$0.01 threshold** (~100 checks)
149
+ - Base L2 gas ≈ $0.0001/tx → gas overhead = **1%** of payout
150
+
151
+ ---
152
+
153
+ ## Storage Architecture (Dual-Write)
154
+
155
+ Every payout writes to **two places simultaneously**:
156
+
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 |
161
+
162
+ ### DB Schema
163
+
164
+ ```sql
165
+ CREATE TABLE worker_credits (
166
+ address TEXT PRIMARY KEY,
167
+ pending_micros BIGINT NOT NULL DEFAULT 0,
168
+ total_earned BIGINT NOT NULL DEFAULT 0,
169
+ total_paid BIGINT NOT NULL DEFAULT 0,
170
+ last_payout_at TIMESTAMPTZ,
171
+ last_payout_tx TEXT,
172
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
173
+ );
174
+
175
+ CREATE TABLE worker_results (
176
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
177
+ task_id TEXT NOT NULL,
178
+ worker_address TEXT NOT NULL,
179
+ timestamp BIGINT NOT NULL,
180
+ success BOOLEAN NOT NULL,
181
+ response_time_ms INTEGER,
182
+ status_code INTEGER,
183
+ reward_micros INTEGER NOT NULL,
184
+ paid BOOLEAN NOT NULL DEFAULT FALSE,
185
+ payout_id UUID,
186
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
187
+ );
188
+ CREATE INDEX ON worker_results (worker_address, paid);
189
+ CREATE INDEX ON worker_results (created_at);
190
+
191
+ CREATE TABLE worker_payouts (
192
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
193
+ worker_address TEXT NOT NULL,
194
+ amount_micros BIGINT NOT NULL,
195
+ result_count INTEGER NOT NULL,
196
+ results_hash TEXT NOT NULL, -- SHA256 of result IDs (also written to calldata)
197
+ tx_hash TEXT, -- USDC transfer tx on Base
198
+ log_tx_hash TEXT, -- 0 ETH calldata log tx on Base
199
+ status TEXT NOT NULL DEFAULT 'pending', -- pending/confirmed/failed
200
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
201
+ );
202
+
203
+ CREATE TABLE wallet_reputation (
204
+ address TEXT PRIMARY KEY,
205
+ total_paid_usd NUMERIC(12,6) NOT NULL DEFAULT 0,
206
+ tier TEXT NOT NULL DEFAULT 'bronze', -- bronze/silver/gold/platinum
207
+ multiplier NUMERIC(4,2) NOT NULL DEFAULT 1.0,
208
+ refreshed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
209
+ );
210
+ ```
211
+
212
+ ### Blockchain Calldata (on every payout)
213
+
214
+ A separate 0 ETH transaction broadcast alongside the USDC transfer:
215
+
216
+ ```typescript
217
+ // to: BLOCKRUN_LOG_ADDRESS (BlockRun's own address)
218
+ // value: 0 ETH
219
+ // calldata: encoded payout receipt
220
+ {
221
+ v: 1,
222
+ type: "worker_payout",
223
+ worker: "0x...",
224
+ amountMicros: 10000,
225
+ resultCount: 100,
226
+ resultsHash: "0xabc...", // SHA256 of result IDs
227
+ payoutId: "uuid",
228
+ payoutTxHash: "0x...",
229
+ ts: 1234567890
230
+ }
231
+ ```
232
+
233
+ **Independent verification:** Anyone can scan Base for txs to `BLOCKRUN_LOG_ADDRESS`, decode calldata, and verify all worker payouts without trusting BlockRun's DB.
234
+
235
+ ---
236
+
237
+ ## Trust & Verification Model
238
+
239
+ Workers are **existing paying ClawRouter users**. The work is trivially cheap:
240
+
241
+ ```javascript
242
+ const res = await fetch(url, { signal: AbortSignal.timeout(10000) })
243
+ return { status: res.status, latency: Date.now() - start }
244
+ ```
245
+
246
+ Cost to do the work: ~10ms, $0.
247
+ Cost to fake: write cheating code, risk ban.
248
+ Reward either way: $0.0001.
249
+
250
+ **No rational incentive to cheat.** Simple EIP-191 signature proves identity. That's sufficient.
251
+
252
+ Future (V2): nonce injection for BlockRun-owned endpoints, spot-check verification for third-party.
253
+
254
+ ---
255
+
256
+ ## All Design Decisions
257
+
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 |
269
+
270
+ ---
271
+
272
+ ## Go-to-Market
273
+
274
+ ### Phase 1: Supply Side (Month 1–2)
275
+ - Ship `CLAWROUTER_WORKER=1` to 1,000 existing users
276
+ - Pilot: 3 hardcoded tasks monitoring BlockRun's own endpoints
277
+ - Target: 50+ active workers, end-to-end payment verified on-chain
278
+
279
+ ### Phase 2: First Buyers (Month 2–3)
280
+ - Buyer dashboard — register any endpoint, choose SLA tier
281
+ - First 10 customers: 30-day free trial
282
+ - Publish node map (marketing)
283
+ - Target: 5 paying customers, $2,500 MRR
284
+
285
+ ### Phase 3: Scale (Month 3–6)
286
+ - Standard/Premium tiers with BlockRun-backed SLA
287
+ - "State of Web3 Uptime" report from aggregated data
288
+ - Coinbase/Base ecosystem partnership
289
+ - Target: $15,000 MRR
290
+
291
+ ---
292
+
293
+ ## Success Metrics
294
+
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 |
303
+
304
+ ---
305
+
306
+ ## Open Questions (V2)
307
+
308
+ 1. Geographic routing — assign tasks by region
309
+ 2. Buyer dashboard — web UI for endpoint config
310
+ 3. Nonce injection — cryptographic proof for owned endpoints
311
+ 4. Worker reputation UI — let workers see their tier and earnings
312
+ 5. Legal — liability for uptime certificates in regulatory filings
313
+
314
+ ---
315
+
316
+ ---
317
+
318
+ # Implementation Plan
319
+
320
+ ## Files to Touch
321
+
322
+ ### ClawRouter
323
+ | File | Action |
324
+ |------|--------|
325
+ | `src/worker/types.ts` | CREATE |
326
+ | `src/worker/checks.ts` | CREATE |
327
+ | `src/worker/index.ts` | CREATE |
328
+ | `src/index.ts` | MODIFY |
329
+
330
+ ### 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 |
338
+ | `src/app/api/v1/worker/results/route.ts` | CREATE |
339
+
340
+ ### Environment Variables
341
+
342
+ **ClawRouter `.env` / shell:**
343
+ ```bash
344
+ CLAWROUTER_WORKER=1
345
+ WORKER_REGION=US-West # optional
346
+ BLOCKRUN_API_BASE=https://blockrun.ai/api # override for local dev
347
+ ```
348
+
349
+ **BlockRun `.env.local`:**
350
+ ```bash
351
+ WORKER_PAYOUT_WALLET_KEY=0x... # treasury signing key — never commit
352
+ BLOCKRUN_LOG_ADDRESS=0x... # BlockRun's own address for calldata logs
353
+ DATABASE_URL=postgres://... # your DB
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Task 1: ClawRouter — Types
359
+
360
+ **File:** `src/worker/types.ts`
361
+
362
+ ```typescript
363
+ 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
371
+ }
372
+
373
+ export interface WorkerResult {
374
+ taskId: string
375
+ workerAddress: string
376
+ timestamp: number
377
+ success: boolean
378
+ responseTimeMs: number
379
+ statusCode?: number
380
+ error?: string
381
+ // EIP-191 signature of JSON.stringify({ taskId, workerAddress, timestamp, success })
382
+ signature: string
383
+ }
384
+
385
+ export interface WorkerStatus {
386
+ address: string
387
+ completedTasks: number
388
+ totalEarnedMicros: number
389
+ lastPollAt?: number
390
+ busy: boolean
391
+ }
392
+ ```
393
+
394
+ **Steps:**
395
+ 1. `mkdir src/worker && touch src/worker/types.ts` — paste content above
396
+ 2. `npx tsc --noEmit` — expect no errors
397
+ 3. `git add src/worker/types.ts && git commit -m "feat(worker): add types"`
398
+
399
+ ---
400
+
401
+ ## Task 2: ClawRouter — HTTP Check Executor
402
+
403
+ **File:** `src/worker/checks.ts`
404
+
405
+ ```typescript
406
+ import type { WorkerTask } from "./types.js"
407
+
408
+ export async function executeHttpCheck(task: WorkerTask): Promise<{
409
+ success: boolean
410
+ responseTimeMs: number
411
+ statusCode?: number
412
+ error?: string
413
+ }> {
414
+ const start = Date.now()
415
+ try {
416
+ const res = await fetch(task.url, {
417
+ method: "GET",
418
+ signal: AbortSignal.timeout(task.timeoutMs),
419
+ redirect: "follow",
420
+ headers: { "User-Agent": "BlockRun-Worker/1.0" },
421
+ })
422
+ return {
423
+ success: res.status === task.expectedStatus,
424
+ responseTimeMs: Date.now() - start,
425
+ statusCode: res.status,
426
+ }
427
+ } catch (err) {
428
+ const isTimeout = err instanceof Error &&
429
+ (err.name === "TimeoutError" || err.name === "AbortError")
430
+ return {
431
+ success: false,
432
+ responseTimeMs: Date.now() - start,
433
+ error: isTimeout
434
+ ? `Timeout after ${task.timeoutMs}ms`
435
+ : err instanceof Error ? err.message : String(err),
436
+ }
437
+ }
438
+ }
439
+
440
+ // Must produce identical JSON on both sides for signature verification
441
+ export function buildSignableMessage(params: {
442
+ taskId: string
443
+ workerAddress: string
444
+ timestamp: number
445
+ success: boolean
446
+ }): string {
447
+ return JSON.stringify({
448
+ taskId: params.taskId,
449
+ workerAddress: params.workerAddress,
450
+ timestamp: params.timestamp,
451
+ success: params.success,
452
+ })
453
+ }
454
+ ```
455
+
456
+ **Steps:**
457
+ 1. Create `src/worker/checks.ts` — paste above
458
+ 2. `npx tsc --noEmit`
459
+ 3. `git add src/worker/checks.ts && git commit -m "feat(worker): add HTTP check executor"`
460
+
461
+ ---
462
+
463
+ ## Task 3: ClawRouter — WorkerNode Class
464
+
465
+ **File:** `src/worker/index.ts`
466
+
467
+ ```typescript
468
+ import { privateKeyToAccount } from "viem/accounts"
469
+ import type { WorkerTask, WorkerResult, WorkerStatus } from "./types.js"
470
+ import { executeHttpCheck, buildSignableMessage } from "./checks.js"
471
+
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"
476
+
477
+ export class WorkerNode {
478
+ private privateKey: `0x${string}`
479
+ private address: string
480
+ private apiBase: string
481
+ private busy = false
482
+ private status: WorkerStatus
483
+
484
+ 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 }
489
+ }
490
+
491
+ 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)
495
+ }
496
+
497
+ getStatus(): WorkerStatus {
498
+ return { ...this.status, busy: this.busy }
499
+ }
500
+
501
+ private async poll(): Promise<void> {
502
+ if (this.busy) return
503
+ let tasks: WorkerTask[] = []
504
+ try {
505
+ tasks = await this.fetchTasks()
506
+ } catch (err) {
507
+ console.error(`[Worker] fetch tasks failed:`, err instanceof Error ? err.message : err)
508
+ return
509
+ }
510
+ if (tasks.length === 0) return
511
+
512
+ this.busy = true
513
+ this.status.lastPollAt = Date.now()
514
+ console.log(`[Worker] Executing ${tasks.length} task(s)`)
515
+ try {
516
+ const results = await this.executeBatch(tasks)
517
+ await this.submitResults(results)
518
+ } finally {
519
+ this.busy = false
520
+ }
521
+ }
522
+
523
+ private async fetchTasks(): Promise<WorkerTask[]> {
524
+ const url = `${this.apiBase}/v1/worker/tasks?address=${this.address}&region=${REGION}`
525
+ const res = await fetch(url, {
526
+ signal: AbortSignal.timeout(10_000),
527
+ 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[]>
531
+ }
532
+
533
+ private async executeBatch(tasks: WorkerTask[]): Promise<WorkerResult[]> {
534
+ const results: WorkerResult[] = []
535
+ 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)
539
+ }
540
+ return results
541
+ }
542
+
543
+ private async executeAndSign(task: WorkerTask): Promise<WorkerResult> {
544
+ const check = await executeHttpCheck(task)
545
+ const timestamp = Date.now()
546
+ const message = buildSignableMessage({
547
+ taskId: task.id,
548
+ workerAddress: this.address,
549
+ timestamp,
550
+ 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 }
555
+ }
556
+
557
+ private async submitResults(results: WorkerResult[]): Promise<void> {
558
+ try {
559
+ const res = await fetch(`${this.apiBase}/v1/worker/results`, {
560
+ method: "POST",
561
+ headers: { "Content-Type": "application/json", "User-Agent": "BlockRun-Worker/1.0" },
562
+ body: JSON.stringify(results),
563
+ 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`)
569
+ } catch (err) {
570
+ console.error(`[Worker] submit error:`, err instanceof Error ? err.message : err)
571
+ }
572
+ }
573
+ }
574
+ ```
575
+
576
+ **Steps:**
577
+ 1. Create `src/worker/index.ts` — paste above
578
+ 2. `npx tsc --noEmit`
579
+ 3. `git add src/worker/index.ts && git commit -m "feat(worker): add WorkerNode class"`
580
+
581
+ ---
582
+
583
+ ## Task 4: ClawRouter — Wire Worker Mode
584
+
585
+ **File:** `src/index.ts` — modify `startProxyInBackground()`.
586
+
587
+ Find this block (~line 423):
588
+ ```typescript
589
+ setActiveProxy(proxy);
590
+ activeProxyHandle = proxy;
591
+ ```
592
+
593
+ Add immediately after:
594
+ ```typescript
595
+ const workerMode =
596
+ process.env.CLAWROUTER_WORKER === "1" ||
597
+ process.argv.includes("--worker")
598
+
599
+ 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}`)
604
+ }
605
+ ```
606
+
607
+ **Steps:**
608
+ 1. Edit `src/index.ts`
609
+ 2. `npx tsc --noEmit`
610
+ 3. `git add src/index.ts && git commit -m "feat(worker): activate WorkerNode on CLAWROUTER_WORKER=1"`
611
+
612
+ ---
613
+
614
+ ## Task 5: BlockRun — DB Schema
615
+
616
+ Run this migration against your DB (Postgres-compatible):
617
+
618
+ ```sql
619
+ CREATE TABLE worker_credits (
620
+ address TEXT PRIMARY KEY,
621
+ pending_micros BIGINT NOT NULL DEFAULT 0,
622
+ total_earned BIGINT NOT NULL DEFAULT 0,
623
+ total_paid BIGINT NOT NULL DEFAULT 0,
624
+ last_payout_at TIMESTAMPTZ,
625
+ last_payout_tx TEXT,
626
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
627
+ );
628
+
629
+ CREATE TABLE worker_results (
630
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
631
+ task_id TEXT NOT NULL,
632
+ worker_address TEXT NOT NULL,
633
+ timestamp BIGINT NOT NULL,
634
+ success BOOLEAN NOT NULL,
635
+ response_time_ms INTEGER,
636
+ status_code INTEGER,
637
+ reward_micros INTEGER NOT NULL,
638
+ paid BOOLEAN NOT NULL DEFAULT FALSE,
639
+ payout_id UUID,
640
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
641
+ );
642
+ CREATE INDEX ON worker_results (worker_address, paid);
643
+ CREATE INDEX ON worker_results (created_at);
644
+
645
+ CREATE TABLE worker_payouts (
646
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
647
+ worker_address TEXT NOT NULL,
648
+ amount_micros BIGINT NOT NULL,
649
+ result_count INTEGER NOT NULL,
650
+ results_hash TEXT NOT NULL,
651
+ tx_hash TEXT,
652
+ log_tx_hash TEXT,
653
+ status TEXT NOT NULL DEFAULT 'pending',
654
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
655
+ );
656
+
657
+ CREATE TABLE wallet_reputation (
658
+ address TEXT PRIMARY KEY,
659
+ total_paid_usd NUMERIC(12,6) NOT NULL DEFAULT 0,
660
+ tier TEXT NOT NULL DEFAULT 'bronze',
661
+ multiplier NUMERIC(4,2) NOT NULL DEFAULT 1.0,
662
+ refreshed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
663
+ );
664
+ ```
665
+
666
+ **Steps:**
667
+ 1. Run migration against dev DB
668
+ 2. Verify all 4 tables exist
669
+ 3. `git commit -m "feat(worker): add DB migration"`
670
+
671
+ ---
672
+
673
+ ## Task 6: BlockRun — Task Registry
674
+
675
+ **File:** `src/lib/worker-tasks.ts`
676
+
677
+ ```typescript
678
+ 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
686
+ }
687
+
688
+ 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
+ ]
693
+
694
+ // Track recent assignments: taskId → [{ workerAddress, assignedAt }]
695
+ // 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
699
+
700
+ 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
+ })
713
+ }
714
+
715
+ export function markAssigned(taskIds: string[], workerAddress: string): void {
716
+ const addr = workerAddress.toLowerCase()
717
+ const now = Date.now()
718
+ for (const id of taskIds) {
719
+ const list = assignments.get(id) ?? []
720
+ list.push({ workerAddress: addr, assignedAt: now })
721
+ assignments.set(id, list)
722
+ }
723
+ }
724
+
725
+ export function getTaskById(taskId: string): WorkerTask | undefined {
726
+ return PILOT_TASKS.find(t => t.id === taskId)
727
+ }
728
+ ```
729
+
730
+ **Steps:**
731
+ 1. Create `src/lib/worker-tasks.ts`
732
+ 2. `npx tsc --noEmit`
733
+ 3. `git commit -m "feat(worker): task registry with 3-worker redundancy"`
734
+
735
+ ---
736
+
737
+ ## Task 7: BlockRun — Credit Ledger
738
+
739
+ **File:** `src/lib/worker-credits.ts`
740
+
741
+ ```typescript
742
+ import { db } from "@/lib/db" // your DB client — swap for actual import
743
+ import { getReputationMultiplier } from "./worker-reputation"
744
+
745
+ const PAYOUT_THRESHOLD_MICROS = 10_000 // $0.01
746
+
747
+ export async function creditWorker(
748
+ workerAddress: string,
749
+ baseRewardMicros: number,
750
+ ): Promise<{ pendingMicros: number; thresholdReached: boolean }> {
751
+ const multiplier = await getReputationMultiplier(workerAddress)
752
+ const earned = Math.floor(baseRewardMicros * multiplier)
753
+
754
+ // Atomic upsert — safe for concurrent Cloud Run instances
755
+ const result = await db.query<{ pending_micros: number }>(`
756
+ INSERT INTO worker_credits (address, pending_micros, total_earned, updated_at)
757
+ VALUES ($1, $2, $2, NOW())
758
+ ON CONFLICT (address) DO UPDATE SET
759
+ pending_micros = worker_credits.pending_micros + $2,
760
+ total_earned = worker_credits.total_earned + $2,
761
+ updated_at = NOW()
762
+ RETURNING pending_micros
763
+ `, [workerAddress.toLowerCase(), earned])
764
+
765
+ const pendingMicros = result.rows[0].pending_micros
766
+ return {
767
+ pendingMicros,
768
+ thresholdReached: pendingMicros >= PAYOUT_THRESHOLD_MICROS,
769
+ }
770
+ }
771
+
772
+ export async function resetPendingCredits(
773
+ workerAddress: string,
774
+ payoutId: string,
775
+ amountMicros: number,
776
+ txHash: string,
777
+ ): Promise<void> {
778
+ await db.query(`
779
+ UPDATE worker_credits SET
780
+ pending_micros = pending_micros - $2,
781
+ total_paid = total_paid + $2,
782
+ last_payout_at = NOW(),
783
+ last_payout_tx = $3,
784
+ updated_at = NOW()
785
+ WHERE address = $1
786
+ `, [workerAddress.toLowerCase(), amountMicros, txHash])
787
+ }
788
+
789
+ export async function getPendingMicros(workerAddress: string): Promise<number> {
790
+ const result = await db.query<{ pending_micros: number }>(
791
+ `SELECT pending_micros FROM worker_credits WHERE address = $1`,
792
+ [workerAddress.toLowerCase()]
793
+ )
794
+ return result.rows[0]?.pending_micros ?? 0
795
+ }
796
+ ```
797
+
798
+ **Steps:**
799
+ 1. Create `src/lib/worker-credits.ts`
800
+ 2. Wire your actual DB client at `@/lib/db` (swap the import)
801
+ 3. `npx tsc --noEmit`
802
+ 4. `git commit -m "feat(worker): credit ledger with atomic upsert"`
803
+
804
+ ---
805
+
806
+ ## Task 8: BlockRun — Reputation Module
807
+
808
+ **File:** `src/lib/worker-reputation.ts`
809
+
810
+ ```typescript
811
+ import { db } from "@/lib/db"
812
+
813
+ const TIERS = [
814
+ { 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
819
+
820
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // refresh daily
821
+
822
+ export async function getReputationMultiplier(workerAddress: string): Promise<number> {
823
+ const rep = await getReputation(workerAddress)
824
+ return rep.multiplier
825
+ }
826
+
827
+ export async function getReputation(workerAddress: string): Promise<{
828
+ tier: string
829
+ totalPaidUsd: number
830
+ multiplier: number
831
+ }> {
832
+ const addr = workerAddress.toLowerCase()
833
+
834
+ const cached = await db.query<{
835
+ tier: string; total_paid_usd: number; multiplier: number; refreshed_at: Date
836
+ }>(
837
+ `SELECT tier, total_paid_usd, multiplier, refreshed_at
838
+ FROM wallet_reputation WHERE address = $1`,
839
+ [addr]
840
+ )
841
+
842
+ if (cached.rows.length > 0) {
843
+ const row = cached.rows[0]
844
+ const age = Date.now() - new Date(row.refreshed_at).getTime()
845
+ if (age < CACHE_TTL_MS) {
846
+ return { tier: row.tier, totalPaidUsd: Number(row.total_paid_usd), multiplier: Number(row.multiplier) }
847
+ }
848
+ }
849
+
850
+ // Cache miss or stale — recompute from GCS logs
851
+ // 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]
854
+
855
+ await db.query(`
856
+ INSERT INTO wallet_reputation (address, total_paid_usd, tier, multiplier, refreshed_at)
857
+ VALUES ($1, $2, $3, $4, NOW())
858
+ ON CONFLICT (address) DO UPDATE SET
859
+ total_paid_usd = $2, tier = $3, multiplier = $4, refreshed_at = NOW()
860
+ `, [addr, totalPaidUsd, tierEntry.tier, tierEntry.multiplier])
861
+
862
+ return { tier: tierEntry.tier, totalPaidUsd, multiplier: tierEntry.multiplier }
863
+ }
864
+
865
+ // Aggregate total USDC paid by this wallet from GCS LLM call logs
866
+ // This reads BlockRun's own data — no third-party reputation service needed
867
+ async function computeTotalPaidFromGCS(walletAddress: string): Promise<number> {
868
+ // TODO (V2): read gs://blockrun-prod-2026-logs/llm-calls/YYYY-MM-DD.jsonl
869
+ // Filter by wallet, sum cost field
870
+ // Pilot: return 0 (all workers start at Bronze)
871
+ return 0
872
+ }
873
+ ```
874
+
875
+ **Steps:**
876
+ 1. Create `src/lib/worker-reputation.ts`
877
+ 2. `computeTotalPaidFromGCS` is a stub for pilot — implement GCS aggregation in V2
878
+ 3. `git commit -m "feat(worker): reputation module with daily cache"`
879
+
880
+ ---
881
+
882
+ ## Task 9: BlockRun — Payout Module
883
+
884
+ **File:** `src/lib/worker-payouts.ts`
885
+
886
+ ```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"
895
+
896
+ const PAYOUT_THRESHOLD_MICROS = 10_000
897
+
898
+ export async function tryPayout(
899
+ workerAddress: string,
900
+ pendingMicros: number,
901
+ resultIds: string[],
902
+ ): Promise<{ paid: boolean; txHash?: string; logTxHash?: string }> {
903
+ if (pendingMicros < PAYOUT_THRESHOLD_MICROS) return { paid: false }
904
+
905
+ const payoutKey = process.env.WORKER_PAYOUT_WALLET_KEY
906
+ if (!payoutKey?.startsWith("0x")) throw new Error("WORKER_PAYOUT_WALLET_KEY not set")
907
+
908
+ const networkConfig = getCurrentNetworkConfig()
909
+ const payoutAccount = privateKeyToAccount(payoutKey as `0x${string}`)
910
+
911
+ // 1. Insert payout record (pending)
912
+ const resultsHash = createHash("sha256").update(resultIds.sort().join(",")).digest("hex")
913
+ const payoutResult = await db.query<{ id: string }>(`
914
+ INSERT INTO worker_payouts (worker_address, amount_micros, result_count, results_hash, status)
915
+ VALUES ($1, $2, $3, $4, 'pending')
916
+ RETURNING id
917
+ `, [workerAddress.toLowerCase(), pendingMicros, resultIds.length, resultsHash])
918
+ const payoutId = payoutResult.rows[0].id
919
+
920
+ // 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}`
925
+
926
+ const signature = await signTypedData({
927
+ privateKey: payoutKey as `0x${string}`,
928
+ domain: {
929
+ name: networkConfig.usdcDomainName,
930
+ version: "2",
931
+ chainId: networkConfig.chainId,
932
+ verifyingContract: networkConfig.usdc,
933
+ },
934
+ types: {
935
+ TransferWithAuthorization: [
936
+ { name: "from", type: "address" },
937
+ { name: "to", type: "address" },
938
+ { name: "value", type: "uint256" },
939
+ { name: "validAfter", type: "uint256" },
940
+ { name: "validBefore", type: "uint256" },
941
+ { name: "nonce", type: "bytes32" },
942
+ ],
943
+ },
944
+ primaryType: "TransferWithAuthorization",
945
+ message: {
946
+ from: payoutAccount.address,
947
+ to: workerAddress as `0x${string}`,
948
+ value: BigInt(pendingMicros),
949
+ validAfter,
950
+ validBefore,
951
+ nonce,
952
+ },
953
+ })
954
+
955
+ // 3. Build x402 payment payload (same format as incoming payments)
956
+ const paymentPayload = {
957
+ x402Version: 1,
958
+ scheme: "exact",
959
+ network: networkConfig.network,
960
+ payload: {
961
+ signature,
962
+ authorization: {
963
+ from: payoutAccount.address,
964
+ to: workerAddress,
965
+ value: String(pendingMicros),
966
+ validAfter: String(validAfter),
967
+ validBefore: String(validBefore),
968
+ nonce,
969
+ },
970
+ },
971
+ }
972
+ const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64")
973
+
974
+ const requirements = {
975
+ scheme: "exact",
976
+ network: networkConfig.network,
977
+ maxAmountRequired: String(pendingMicros),
978
+ resource: `worker-payout:${workerAddress}`,
979
+ description: "Worker node payout",
980
+ mimeType: "application/json",
981
+ payTo: workerAddress,
982
+ maxTimeoutSeconds: 3600,
983
+ asset: networkConfig.usdc,
984
+ outputSchema: null,
985
+ extra: null,
986
+ }
987
+
988
+ // 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}`)
991
+
992
+ const txHash = settled.txHash!
993
+
994
+ // 5. Write calldata log tx to Base (audit trail — failure does NOT block payout)
995
+ const logTxHash = await writeCalldataLog({
996
+ workerAddress, amountMicros: pendingMicros,
997
+ resultCount: resultIds.length, resultsHash, payoutId,
998
+ payoutTxHash: txHash, payoutKey, networkConfig,
999
+ })
1000
+
1001
+ // 6. Update DB: mark payout confirmed, reset credits, mark results paid
1002
+ await Promise.all([
1003
+ db.query(
1004
+ `UPDATE worker_payouts SET status='confirmed', tx_hash=$1, log_tx_hash=$2 WHERE id=$3`,
1005
+ [txHash, logTxHash, payoutId]
1006
+ ),
1007
+ 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
+ ])
1013
+
1014
+ return { paid: true, txHash, logTxHash }
1015
+ }
1016
+
1017
+ 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>
1026
+ }): Promise<string | undefined> {
1027
+ const logAddress = process.env.BLOCKRUN_LOG_ADDRESS as `0x${string}` | undefined
1028
+ if (!logAddress) return undefined
1029
+
1030
+ 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")
1047
+
1048
+ return await walletClient.sendTransaction({
1049
+ to: logAddress,
1050
+ value: BigInt(0),
1051
+ data: `0x${data}` as `0x${string}`,
1052
+ })
1053
+ } catch (err) {
1054
+ console.error("[Worker Payout] calldata log tx failed:", err)
1055
+ return undefined
1056
+ }
1057
+ }
1058
+ ```
1059
+
1060
+ **Steps:**
1061
+ 1. Create `src/lib/worker-payouts.ts`
1062
+ 2. Check `network-config.ts` — add `chainId: 8453 / 84532` if missing
1063
+ 3. `npx tsc --noEmit`
1064
+ 4. `git commit -m "feat(worker): payout module with x402 + calldata audit log"`
1065
+
1066
+ ---
1067
+
1068
+ ## Task 10: BlockRun — GET /api/v1/worker/tasks
1069
+
1070
+ **File:** `src/app/api/v1/worker/tasks/route.ts`
1071
+
1072
+ ```typescript
1073
+ import { NextRequest, NextResponse } from "next/server"
1074
+ import { getTasksForWorker, markAssigned } from "@/lib/worker-tasks"
1075
+
1076
+ export const runtime = "nodejs"
1077
+
1078
+ 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
1082
+
1083
+ if (!address?.startsWith("0x")) {
1084
+ return NextResponse.json({ error: "address required" }, { status: 400 })
1085
+ }
1086
+
1087
+ const tasks = getTasksForWorker(address, region)
1088
+ markAssigned(tasks.map(t => t.id), address)
1089
+
1090
+ return NextResponse.json(tasks)
1091
+ }
1092
+ ```
1093
+
1094
+ **Steps:**
1095
+ 1. `mkdir -p src/app/api/v1/worker/tasks && touch route.ts`
1096
+ 2. Test: `curl "http://localhost:3000/api/v1/worker/tasks?address=0x000..."`
1097
+ 3. Expect: JSON array with up to 3 pilot tasks
1098
+ 4. `git commit -m "feat(worker): GET /api/v1/worker/tasks"`
1099
+
1100
+ ---
1101
+
1102
+ ## Task 11: BlockRun — POST /api/v1/worker/results
1103
+
1104
+ **File:** `src/app/api/v1/worker/results/route.ts`
1105
+
1106
+ ```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"
1113
+
1114
+ export const runtime = "nodejs"
1115
+
1116
+ 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
1125
+ }
1126
+
1127
+ export async function POST(request: NextRequest) {
1128
+ let results: WorkerResult[]
1129
+ try {
1130
+ results = await request.json()
1131
+ } catch {
1132
+ return NextResponse.json({ error: "invalid JSON" }, { status: 400 })
1133
+ }
1134
+
1135
+ if (!Array.isArray(results) || results.length === 0 || results.length > 50) {
1136
+ return NextResponse.json({ error: "results must be array of 1–50" }, { status: 400 })
1137
+ }
1138
+
1139
+ let accepted = 0
1140
+ let totalEarnedMicros = 0
1141
+ const errors: string[] = []
1142
+ const acceptedResultIds: string[] = []
1143
+
1144
+ for (const result of results) {
1145
+ try {
1146
+ const task = getTaskById(result.taskId)
1147
+ if (!task) { errors.push(`${result.taskId}: unknown task`); continue }
1148
+
1149
+ // Verify EIP-191 signature
1150
+ const message = JSON.stringify({
1151
+ taskId: result.taskId,
1152
+ workerAddress: result.workerAddress,
1153
+ timestamp: result.timestamp,
1154
+ success: result.success,
1155
+ })
1156
+ const recovered = await recoverMessageAddress({
1157
+ message,
1158
+ signature: result.signature as `0x${string}`,
1159
+ })
1160
+ if (recovered.toLowerCase() !== result.workerAddress.toLowerCase()) {
1161
+ errors.push(`${result.taskId}: invalid signature`); continue
1162
+ }
1163
+
1164
+ // Freshness check (5 min max)
1165
+ if (Date.now() - result.timestamp > 5 * 60 * 1000) {
1166
+ errors.push(`${result.taskId}: result too old`); continue
1167
+ }
1168
+
1169
+ // Write result to DB
1170
+ const insertResult = await db.query<{ id: string }>(`
1171
+ INSERT INTO worker_results
1172
+ (task_id, worker_address, timestamp, success, response_time_ms, status_code, reward_micros)
1173
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
1174
+ 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)
1185
+
1186
+ // Credit worker (with reputation multiplier)
1187
+ const { pendingMicros, thresholdReached } = await creditWorker(result.workerAddress, task.rewardMicros)
1188
+
1189
+ // Trigger payout if threshold reached (fire and forget)
1190
+ if (thresholdReached) {
1191
+ tryPayout(result.workerAddress, pendingMicros, acceptedResultIds)
1192
+ .catch(err => console.error(`[Worker Payout] failed for ${result.workerAddress}:`, err))
1193
+ }
1194
+
1195
+ accepted++
1196
+ totalEarnedMicros += task.rewardMicros
1197
+
1198
+ } catch (err) {
1199
+ errors.push(`${result.taskId}: ${err instanceof Error ? err.message : String(err)}`)
1200
+ }
1201
+ }
1202
+
1203
+ return NextResponse.json({
1204
+ accepted,
1205
+ earned: (totalEarnedMicros / 1_000_000).toFixed(6),
1206
+ errors: errors.length > 0 ? errors : undefined,
1207
+ })
1208
+ }
1209
+ ```
1210
+
1211
+ **Steps:**
1212
+ 1. `mkdir -p src/app/api/v1/worker/results && touch route.ts`
1213
+ 2. `npx tsc --noEmit`
1214
+ 3. `git commit -m "feat(worker): POST /api/v1/worker/results with sig verify, DB write, payout trigger"`
1215
+
1216
+ ---
1217
+
1218
+ ## End-to-End Test
1219
+
1220
+ ```bash
1221
+ # 1. Start BlockRun locally
1222
+ cd /Users/vickyfu/Documents/blockrun-web/blockrun
1223
+ pnpm dev
1224
+
1225
+ # 2. Verify tasks endpoint
1226
+ curl "http://localhost:3000/api/v1/worker/tasks?address=0x0000000000000000000000000000000000000001"
1227
+ # → JSON array with 3 tasks
1228
+
1229
+ # 3. Start ClawRouter in worker mode (pointed at localhost)
1230
+ cd /Users/vickyfu/Documents/blockrun-web/ClawRouter
1231
+ CLAWROUTER_WORKER=1 BLOCKRUN_API_BASE=http://localhost:3000/api npx openclaw gateway start
1232
+
1233
+ # 4. Watch for logs:
1234
+ # [Worker] Starting — 0x... region=unknown
1235
+ # [Worker] Executing 3 task(s)
1236
+ # [Worker] ✓ 3 result(s) accepted, earned $0.000300 USDC
1237
+
1238
+ # 5. Check DB: worker_results and worker_credits tables populated
1239
+ # 6. At $0.01 threshold (~100 checks): worker_payouts row created, USDC transferred
1240
+ # 7. Check Base explorer: calldata log tx visible on BLOCKRUN_LOG_ADDRESS
1241
+ ```