@blockrun/clawrouter 0.12.64 → 0.12.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -55
- package/dist/cli.js +70 -25
- package/dist/cli.js.map +1 -1
- package/dist/index.js +77 -27
- package/dist/index.js.map +1 -1
- package/docs/anthropic-cost-savings.md +90 -85
- package/docs/architecture.md +12 -12
- package/docs/{blog-openclaw-cost-overruns.md → clawrouter-cuts-llm-api-costs-500x.md} +27 -27
- package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +280 -0
- package/docs/configuration.md +2 -2
- package/docs/image-generation.md +39 -39
- package/docs/{blog-benchmark-2026-03.md → llm-router-benchmark-46-models-sub-1ms-routing.md} +61 -64
- package/docs/routing-profiles.md +6 -6
- package/docs/{technical-routing-2026-03.md → smart-llm-router-14-dimension-classifier.md} +29 -28
- package/docs/worker-network.md +438 -347
- package/package.json +1 -1
- package/scripts/reinstall.sh +31 -6
- package/scripts/update.sh +6 -1
- package/docs/vs-openrouter.md +0 -157
package/docs/worker-network.md
CHANGED
|
@@ -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
|
|
47
|
-
|
|
48
|
-
| **Best Effort** | Checks run when workers online (~90% coverage) | $0.0003/check | 67%
|
|
49
|
-
| **Standard**
|
|
50
|
-
| **Premium**
|
|
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
|
|
79
|
-
|
|
80
|
-
| **Bronze**
|
|
81
|
-
| **Silver**
|
|
82
|
-
| **Gold**
|
|
83
|
-
| **Platinum** | ≥ $200 paid | $0.0002/check (2x)
|
|
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
|
|
158
|
-
|
|
159
|
-
| **DB**
|
|
160
|
-
| **Base calldata** | Immutable audit trail, independent verification
|
|
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
|
|
259
|
-
|
|
260
|
-
| 3-worker consensus needed? | No — redundancy only, not verification
|
|
261
|
-
| How to pay workers?
|
|
262
|
-
| Workers always online?
|
|
263
|
-
| Verify work authenticity?
|
|
264
|
-
| Track credits per worker?
|
|
265
|
-
| Pay per check on-chain?
|
|
266
|
-
| Calldata mechanism?
|
|
267
|
-
| Reputation source?
|
|
268
|
-
| DB choice?
|
|
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
|
|
296
|
-
|
|
297
|
-
| Active workers
|
|
298
|
-
| Monitored endpoints
|
|
299
|
-
| Paying customers
|
|
300
|
-
| MRR
|
|
301
|
-
| USDC to workers/month | $250
|
|
302
|
-
| On-chain payout txs
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
|
333
|
+
|
|
334
|
+
| File | Action |
|
|
335
|
+
| ---------------------- | ------ |
|
|
336
|
+
| `src/worker/types.ts` | CREATE |
|
|
326
337
|
| `src/worker/checks.ts` | CREATE |
|
|
327
|
-
| `src/worker/index.ts`
|
|
328
|
-
| `src/index.ts`
|
|
338
|
+
| `src/worker/index.ts` | CREATE |
|
|
339
|
+
| `src/index.ts` | MODIFY |
|
|
329
340
|
|
|
330
341
|
### BlockRun
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
|
334
|
-
| `src/lib/worker-
|
|
335
|
-
| `src/lib/worker-
|
|
336
|
-
| `src/lib/worker-
|
|
337
|
-
| `src/
|
|
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 =
|
|
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
|
|
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}®ion=${REGION}
|
|
542
|
+
const url = `${this.apiBase}/v1/worker/tasks?address=${this.address}®ion=${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) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
{
|
|
690
|
-
|
|
691
|
-
|
|
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"
|
|
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
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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",
|
|
816
|
-
{ tier: "silver",
|
|
817
|
-
{ tier: "bronze",
|
|
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
|
|
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;
|
|
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 {
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
918
|
-
|
|
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",
|
|
937
|
-
{ name: "to",
|
|
938
|
-
{ name: "value",
|
|
939
|
-
{ name: "validAfter",
|
|
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",
|
|
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,
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
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(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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(
|
|
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) {
|
|
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`);
|
|
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`);
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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(
|
|
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
|
-
|
|
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"`
|