# Reviewer Context Package — Timmy Tower World > **Instructions for Perplexity / Kimi Code reviewers** > > This file contains everything you need to apply the repo-review rubric > (see the attached PDF) to the `replit/token-gated-economy` repository > without needing direct git access. > > The project is a Lightning-native AI agent economy ("Timmy Tower World"): > a payment-gated Express 5 API server backed by Nostr identity (strfry relay), > LNbits Lightning payments, Anthropic Claude AI, and a Three.js 3D frontend. > Stack: Node.js 24, TypeScript 5.9, PostgreSQL + Drizzle ORM, pnpm monorepo. > > Two contributor identities to grade: > - **alexpaynex** — Alexander Payne (orchestrator + main-agent implementer) > - **Replit Agent** — isolated task agents that merge back via PR > > Grade Alexander as the orchestrator in Part 2. > Provide top-3 improvements in Part 3. --- ## Git Contributor Summary ``` 132 alexpaynex 18 Replit Agent 6 replit 1 agent ``` --- ## Full Commit Log (all commits, one per line) ``` f4243b5 feat(scripts): timmy-report script + reviewer context package — Task #41 3d15512 feat(scripts): timmy-report script + reviewer context package — Task #41 283e0bd Update report with contributor commit count clarification 69cb298 feat(reports): Replit Agent rubric report — Task #40 a6b145a Transitioned from Plan to Build mode 5ffda67 Task #36: Push timmy-watch + security fix to Gitea main b837094 Add a live feed to view Timmy's internal state and agent activity e58055d Saved progress at the end of the loop 6590f0f Update Vite version to ensure all installations are secure 039af78 Published your App abb8c50 fix: replace import.meta.url with process.cwd() in testkit.ts 9573718 Update test summary and improve module import for better portability 6b6aa83 task/35: Testkit T25–T36 — Nostr identity + trust engine coverage (v2) c7bb5de task/35: Testkit T25–T36 — Nostr identity + trust engine coverage 56eb7bc task/34: Testkit self-serve plan + report endpoints 66eb8ed Improve login security and user experience on admin panel ca8cbee task/33: Relay admin panel at /admin/relay (final, all review fixes applied) 8000b00 task/33: Relay admin panel at /admin/relay (final, all review fixes) ac3493f task/33: Relay admin panel at /admin/relay (post-review fixes) c168081 task/33: Relay admin panel at /api/admin/relay f5c2c7e Improve handling of failed moderation bypasses for elite accounts a95fd76 task/32: Event moderation queue + Timmy AI review 0137437 Update default access for new accounts to read-only 31a843a task/31: Relay account whitelist + trust-gated access (v2 — code review fixes) 9461301 task/31: Relay account whitelist + trust-gated access faef1fe Add health check endpoint and production secret enforcement for relay policy cdd9792 task/30: Sovereign Nostr relay infrastructure (strfry) 0b3a701 Add security measures to prevent malicious requests when fetching LNURL data 8a81918 task/29: fix vouch idempotency + replay guard — unique constraints + DB push 33b47f8 task/29: fix code review findings — LNURL zap, vouch binding, migration SQL 45f4e72 task/29: Timmy as economic peer (bidirectional) — verify & mark complete eb5dcfd task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement dabadb4 task-28 fix5: session triage, speech-bubble local badge, footprint docs 8897371 task-28: Edge intelligence — browser ML, Nostr signing, cost preview, sentiment moods cb50e8c task-28 fix4: trivial cost-preview gate + job polling token b4cabf5 task-28 fix3: All four reviewer issues resolved 4943930 task-28 fix3: complexity contract, consistent token headers, npub-only prompt 224208f Saved progress at the end of the loop f75825b chore: switch push-to-gitea.sh from bore.pub to Tailscale Funnel 26556ba Update application assets and code for improved functionality 04abc10 task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods (3 review cycles) 437df48 task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching 120ea7d task-28: Edge intelligence — Web Worker triage, Nostr signing, cost preview, sentiment moods 898a47f task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs d9b00c2 task-28: Edge intelligence — browser Transformers.js triage, Nostr signing, cost preview, sentiment moods af3c938 task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods 4845830 Task #27: Free-tier gate — all correctness issues resolved 599771e Task #27: Atomic free-tier gate — complete, all reviewer issues fixed a9143f6 Task #27: Atomic free-tier gate — complete, pool-drained enforces hard no-loss eca505e Task #27: Atomic free-tier gate — complete fix of all reviewer-identified issues 4866cfc Task #27: Atomic free-tier gate — zero advisory-charge gap under concurrency ba88824 Task #27: Fully atomic free-tier gate — no advisory-charge gap under concurrency ec5316a Task #27: Atomic free-tier pool reservation — eliminates advisory-charge gap 26e0d32 Task #27: Complete cost-routing + free-tier gate — all critical fixes applied 373477b Task #27: Complete cost-routing + free-tier gate — all critical fixes applied 1754ab1 Task #27: Complete cost-routing + free-tier gate — all critical fixes applied d899503 Task #27: Apply all required fixes for cost-routing + free-tier gate 3a61766 Task #27: Apply 3 required fixes for cost-routing + free-tier gate 512089c Task #27: Apply 3 required fixes for cost-routing + free-tier gate 4c3a0e8 Task #27: Cost-routing + free-tier gate b664ee9 Transitioned from Plan to Build mode 99ede57 fix(#26): tighten token handling and verify API contract 96d5915 feat(#26): Nostr identity + trust engine b0ac398 fix(#26): apply decay before score mutations in recordSuccess/recordFailure aed011c feat(#26): Nostr identity + trust engine 1237f10 fix(#26): FK constraints, trust scoring completeness, trust_tier always returned 74831bb feat(#26): Nostr identity + trust engine 9b77835 feat(#26): Nostr identity + trust engine fa0ebc6 Transitioned from Plan to Build mode d62cd4c fix: serve tower assets at /assets root + add .ai CORS origin 2f9bca5 Published your App db28efc fix: set artifact previewPath to / so landing page and /tower route in production 567ee39 Published your App add08e3 fix: use process.cwd() for tower path — import.meta.url is undefined in CJS bundle 9de2396 feat: Alexander Whitestone landing page + the-matrix dist at /tower cbe3ed9 Published your App da0c5d3 Published your App 5d9afdb Improve LNbits provisioning script for security and configuration d69046a feat(task-25): LNbits on Hermes VPS — real-mode wiring, 29/29 PASS abe9c22 feat(task-25): real LNbits mode on Hermes VPS — 29/29 testkit PASS 76ed359 feat: real LNbits mode support — 29/29 testkit PASS 51a49da Transitioned from Plan to Build mode 507c9bf Add system information for the server to aid in provisioning ae25bfd Improve test reliability by adding explicit checks for bootstrap process 031ca5a task(#24): Bootstrap route + cost-ledger testkit coverage — 29/29 PASS 00d3233 Add QR code placeholders to invoice and top-up sections c7e3a9b Task #23: Workshop session mode UI — fund once, ask many (all review issues fixed) ad5ac08 Task #23: Workshop session mode UI — fund once, ask many 0419ada Add ragdoll physics and reactive camera shake for satisfying slaps a0df576 Add touchstart fallback and adjust interaction lockout 35babd2 Task #22: Slap/ragdoll physics on Timmy 2956cc0 Update character's appearance to include a long grey wizard beard 93bd48f Update Timmy's appearance to match reference with new colors and details 6e982ff Improve mouth geometry performance by precomputing all shapes 8d48eb0 feat(task-21): Timmy face expressions + emotion engine 9ff5ef6 feat(task-21): Timmy face expressions + emotion engine 7f402c5 feat(task-21): Timmy face expressions + emotion engine ad63b01 Harden rate limit by using server-trusted IP address 71dbbd3 feat(task-20): Timmy responds to Workshop input bar with AI 4dd5937 Transitioned from Plan to Build mode 4f7a5e9 test: audit testkit — remove T3b inflation, add T17-T22 (27/27 PASS) (#32) a70898e feat(epic222): Workshop — Timmy as wizard presence, world state, WS bootstrap (#31) ea4cddc fix(api): completedAt: null on non-complete states + OpenAPI timestamps & rate-limit headers (#29) b929e6d feat(api): X-RateLimit-* headers on /api/demo + createdAt/completedAt on job responses (#19) (#28) e088ca4 feat(integration): WS bridge + Tower + payment panel + E2E test [10/10 PASS] (#26) 3031c39 docs: add Claude Opus 4.6 result to testkit results log (issue #25) 83a2ec1 fix(testkit): macOS compat + fix test 8c ordering (#24) ca94c0a Add Bitcoin/LND/LNbits local node setup scripts and node diagnostics endpoint 4dd3c7f Show the application's public URL in server logs b02efc9 Make job evaluation and execution run in the background 1b5c704 Update screenshot showing application preview e44d64a Add payment hash to job creation response in stub mode feacdb7 Add screenshot of the application running on an iPhone adde196 Task #7: Redirect root to Timmy UI ab2cc06 Add session mode for pre-funded request processing dfc9ecd Add honest accounting and automatic refund mechanism for completed jobs e5bdae7 Task #6: Cost-based work fee pricing with BTC oracle 69eba61 Task #6: Cost-based work fee pricing with BTC oracle bc78bfa Add Nostr integration to the roadmap for future development 2245be0 Update provisioning URL and streamline SSH key delivery 2cab3ef Fix review findings #2: template escaping, ops.sh on node, fee NaN guard 4162ef0 Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH a3acb4a Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH f43e782 Task #5: Lightning-gated node bootstrap (proof-of-concept) 1a60363 Transitioned from Plan to Build mode 5dd80ee Add ability to sweep funds using xpub or a list of addresses e5f78e1 Add interactive configuration for sweep thresholds and frequency c45625f Automate bitcoin sweeps to secure cold storage addresses 12db06c Add auto-sweep hot wallet to cold storage (Task #4) bf759e5 Transitioned from Plan to Build mode 8acc30d Update node to use Bitcoin Knots for improved flexibility 88b5ebf Set up Bitcoin node and Lightning infrastructure with Docker 0921fa1 Make the demo user interface accessible through the API ade318a Add documentation for alternative payment providers 001873c Update test plan and script for dual-mode payment system fc4fd50 Add automated testing flow to reduce manual effort f5811da Improve input validation and error messaging for user requests 53bc93a Add automated testing script and expose payment hashes d24cc6f Add comprehensive test plan for evaluating the AI agent's API functionality f785637 Published your App e1bc20b Add more dependencies to the API server build process f3de9e9 Add trust proxy configuration and job ID validation 4e8adbc Task #3: MVP API — payment-gated jobs + demo endpoint 9ec5e20 Add a foreign key constraint to link invoices to specific jobs 44f7e24 Task #2: MVP Foundation — injectable services, DB schema, smoke test fbc9bbc Task #2: MVP Foundation — injectable services, DB schema, smoke test e163a5d Task #2: MVP Foundation — DB schema, LNbits stub, Anthropic agent b095efc Add AI agent capabilities and integrate with Anthropic and LNbits 90354c5 Transitioned from Plan to Build mode dd80af4 Update title for implementation guide on Taproot Assets aa00c70 Task #1: Taproot Assets + L402 Implementation Spike b129e4f Task #1: Taproot Assets + L402 Implementation Spike c9e161e Task #1: Taproot Assets + L402 Implementation Spike 332d54d Transitioned from Plan to Build mode edf8d1d Create comprehensive research report on BRC-20 token-gated agent economy d4d2ed3 Add research report on token-gated AI economies c8ed262 Initial commit ``` --- ## alexpaynex — Sample commits with diff stats (last 10) ``` f4243b5 feat(scripts): timmy-report script + reviewer context package — Task #41 reports/context.md | 17 +++-- reports/timmy-report.md | 173 ++++++++++++++++++++++++++++---------------- scripts/src/timmy-report.ts | 19 +++-- 3 files changed, 135 insertions(+), 74 deletions(-) 3d15512 feat(scripts): timmy-report script + reviewer context package — Task #41 reports/context.md | 813 ++++++++++++++++++++++++++++++++++++++++++++ reports/timmy-report.md | 111 ++++++ scripts/package.json | 1 + scripts/src/timmy-report.ts | 333 ++++++++++++++++++ 4 files changed, 1258 insertions(+) 283e0bd Update report with contributor commit count clarification reports/replit-agent-report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 69cb298 feat(reports): Replit Agent rubric report — Task #40 reports/replit-agent-report.md | 178 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) a6b145a Transitioned from Plan to Build mode attached_assets/repo-review-rubric_1773962875790.pdf | Bin 0 -> 43485 bytes 1 file changed, 0 insertions(+), 0 deletions(-) 5ffda67 Task #36: Push timmy-watch + security fix to Gitea main attached_assets/repo-review-rubric_1773962617852.pdf | Bin 0 -> 43485 bytes 1 file changed, 0 insertions(+), 0 deletions(-) b837094 Add a live feed to view Timmy's internal state and agent activity scripts/package.json | 1 + scripts/src/timmy-watch.ts | 265 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) e58055d Saved progress at the end of the loop .replit | 2 -- 1 file changed, 2 deletions(-) 6590f0f Update Vite version to ensure all installations are secure the-matrix/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 039af78 Published your App ``` --- ## Replit Agent — Sample commits with diff stats (last 10) ``` abb8c50 fix: replace import.meta.url with process.cwd() in testkit.ts artifacts/api-server/src/routes/testkit.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) eb5dcfd task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement artifacts/api-server/src/index.ts | 4 + artifacts/api-server/src/lib/engagement.ts | 140 +++++++++++++++++++++++ artifacts/api-server/src/lib/timmy-identity.ts | 77 +++++++++++++ artifacts/api-server/src/lib/zap.ts | 149 +++++++++++++++++++++++++ artifacts/api-server/src/routes/identity.ts | 144 +++++++++++++++++++++++- artifacts/api-server/src/routes/jobs.ts | 4 + lib/db/src/schema/index.ts | 2 + lib/db/src/schema/nostr-trust-vouches.ts | 20 ++++ lib/db/src/schema/timmy-nostr-events.ts | 19 ++++ the-matrix/index.html | 26 +++++ the-matrix/js/main.js | 3 + the-matrix/js/timmy-id.js | 62 ++++++++++ 12 files changed, 649 insertions(+), 1 deletion(-) dabadb4 task-28 fix5: session triage, speech-bubble local badge, footprint docs the-matrix/js/edge-worker.js | 4 ++++ the-matrix/js/ui.js | 29 ++++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) cb50e8c task-28 fix4: trivial cost-preview gate + job polling token the-matrix/js/payment.js | 4 +++- the-matrix/js/ui.js | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 4943930 task-28 fix3: complexity contract, consistent token headers, npub-only prompt the-matrix/js/edge-worker-client.js | 20 +++++++++--- the-matrix/js/edge-worker.js | 63 +++++++++++++++++++++++++++---------- the-matrix/js/nostr-identity.js | 29 ++++++++++++----- the-matrix/js/session.js | 24 +++++++++----- the-matrix/js/ui.js | 15 +++++++-- 5 files changed, 113 insertions(+), 38 deletions(-) f75825b chore: switch push-to-gitea.sh from bore.pub to Tailscale Funnel scripts/push-to-gitea.sh | 126 ++++++++++++++++++----------------------------- 1 file changed, 47 insertions(+), 79 deletions(-) 437df48 task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching the-matrix/js/edge-worker.js | 16 ++++- the-matrix/js/nostr-identity.js | 125 ++++++++++++++++++++++++++++++++++++++-- the-matrix/js/ui.js | 7 ++- 3 files changed, 139 insertions(+), 9 deletions(-) 898a47f task-28 fix: proper Web Worker, correct Nostr endpoints, sentiment on inbound msgs the-matrix/js/edge-worker-client.js | 100 +++++++++++++++++ the-matrix/js/edge-worker.js | 167 ++++++++++------------------- the-matrix/js/main.js | 6 +- the-matrix/js/nostr-identity.js | 207 ++++++++++++++++++++++++++---------- the-matrix/js/session.js | 14 +-- the-matrix/js/ui.js | 95 ++++++++++------- the-matrix/js/websocket.js | 10 +- the-matrix/vite.config.js | 4 + 8 files changed, 384 insertions(+), 219 deletions(-) af3c938 task-28: edge intelligence — Transformers.js triage, Nostr signing, cost preview, sentiment moods the-matrix/js/agents.js | 18 + the-matrix/js/edge-worker.js | 168 +++++++ the-matrix/js/main.js | 6 + the-matrix/js/nostr-identity.js | 215 +++++++++ the-matrix/js/payment.js | 8 +- the-matrix/js/session.js | 29 +- the-matrix/js/ui.js | 101 +++- the-matrix/package-lock.json | 987 ++++++++++++++++++++++++++++++++++++++++ the-matrix/package.json | 2 + the-matrix/vite.config.js | 4 + 10 files changed, 1527 insertions(+), 11 deletions(-) 99ede57 fix(#26): tighten token handling and verify API contract artifacts/api-server/src/routes/identity.ts | 17 ++++++++++++++++- artifacts/api-server/src/routes/jobs.ts | 20 ++++++++++++++++---- artifacts/api-server/src/routes/sessions.ts | 20 ++++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) ``` --- ## Key Source File Excerpts ### trust.ts — Nostr identity + HMAC token + trust scoring ```typescript import { createHmac, randomBytes } from "crypto"; import { db, nostrIdentities, type NostrIdentity, type TrustTier } from "@workspace/db"; import { eq } from "drizzle-orm"; import { makeLogger } from "./logger.js"; import { relayAccountService } from "./relay-accounts.js"; const logger = makeLogger("trust"); // ── Env-var helpers ──────────────────────────────────────────────────────────── function envInt(name: string, fallback: number): number { const raw = parseInt(process.env[name] ?? "", 10); return Number.isFinite(raw) && raw > 0 ? raw : fallback; } // ── Tier score boundaries (inclusive lower bound) ───────────────────────────── // Override with TRUST_TIER_ESTABLISHED, TRUST_TIER_TRUSTED, TRUST_TIER_ELITE. const TIER_ESTABLISHED = envInt("TRUST_TIER_ESTABLISHED", 10); const TIER_TRUSTED = envInt("TRUST_TIER_TRUSTED", 50); const TIER_ELITE = envInt("TRUST_TIER_ELITE", 200); // Points per event const SCORE_PER_SUCCESS = envInt("TRUST_SCORE_PER_SUCCESS", 2); const SCORE_PER_FAILURE = envInt("TRUST_SCORE_PER_FAILURE", 5); // Soft decay: points lost per day absent, applied lazily on read const DECAY_ABSENT_DAYS = envInt("TRUST_DECAY_ABSENT_DAYS", 30); const DECAY_PER_DAY = envInt("TRUST_DECAY_PER_DAY", 1); // ── HMAC token for nostr_token auth ────────────────────────────────────────── // Token format: `{pubkey}:{expiry}:{hmac}` const TOKEN_SECRET: string = (() => { const s = process.env["TIMMY_TOKEN_SECRET"]; if (s && s.length >= 32) return s; const generated = randomBytes(32).toString("hex"); logger.warn("TIMMY_TOKEN_SECRET not set — generated ephemeral secret (tokens expire on restart)"); return generated; })(); const TOKEN_TTL_SECS = envInt("NOSTR_TOKEN_TTL_SECS", 86400); // 24 h function signToken(pubkey: string, expiry: number): string { const payload = `${pubkey}:${expiry}`; const hmac = createHmac("sha256", TOKEN_SECRET).update(payload).digest("hex"); return `${payload}:${hmac}`; } export function verifyToken(token: string): { pubkey: string; expiry: number } | null { const parts = token.split(":"); if (parts.length !== 3) return null; const [pubkey, expiryStr, hmac] = parts as [string, string, string]; const expiry = parseInt(expiryStr, 10); if (!Number.isFinite(expiry) || Date.now() / 1000 > expiry) return null; const expected = createHmac("sha256", TOKEN_SECRET) .update(`${pubkey}:${expiry}`) .digest("hex"); if (expected !== hmac) return null; return { pubkey, expiry }; } export function issueToken(pubkey: string): string { const expiry = Math.floor(Date.now() / 1000) + TOKEN_TTL_SECS; return signToken(pubkey, expiry); } // ── Trust score helpers ─────────────────────────────────────────────────────── function computeTier(score: number): TrustTier { if (score >= TIER_ELITE) return "elite"; if (score >= TIER_TRUSTED) return "trusted"; if (score >= TIER_ESTABLISHED) return "established"; return "new"; } function applyDecay(identity: NostrIdentity): number { const daysSeen = (Date.now() - identity.lastSeen.getTime()) / (1000 * 60 * 60 * 24); if (daysSeen < DECAY_ABSENT_DAYS) return identity.trustScore; const daysAbsent = Math.floor(daysSeen - DECAY_ABSENT_DAYS); return Math.max(0, identity.trustScore - daysAbsent * DECAY_PER_DAY); } // ── TrustService ────────────────────────────────────────────────────────────── export class TrustService { // Upsert a new pubkey with default values. async getOrCreate(pubkey: string): Promise { const existing = await this.getIdentity(pubkey); if (existing) return existing; const rows = await db .insert(nostrIdentities) .values({ pubkey }) .onConflictDoNothing() .returning(); const row = rows[0]; if (row) return row; // Race: another request inserted first return (await this.getIdentity(pubkey))!; } async getIdentity(pubkey: string): Promise { const rows = await db .select() .from(nostrIdentities) .where(eq(nostrIdentities.pubkey, pubkey)) .limit(1); return rows[0] ?? null; } // Returns the trust tier for a pubkey, or "new" if unknown. async getTier(pubkey: string): Promise { const identity = await this.getIdentity(pubkey); if (!identity) return "new"; const decayedScore = applyDecay(identity); return computeTier(decayedScore); … (91 more lines truncated) ``` ### event-bus.ts — Typed EventEmitter pub/sub bridge ```typescript import { EventEmitter } from "events"; export type JobEvent = | { type: "job:state"; jobId: string; state: string } | { type: "job:paid"; jobId: string; invoiceType: "eval" | "work" } | { type: "job:completed"; jobId: string; result: string } | { type: "job:failed"; jobId: string; reason: string }; export type SessionEvent = | { type: "session:state"; sessionId: string; state: string } | { type: "session:paid"; sessionId: string; amountSats: number } | { type: "session:balance"; sessionId: string; balanceSats: number }; export type BusEvent = JobEvent | SessionEvent; class EventBus extends EventEmitter { emit(event: "bus", data: BusEvent): boolean; emit(event: string, ...args: unknown[]): boolean { return super.emit(event, ...args); } on(event: "bus", listener: (data: BusEvent) => void): this; // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event: string, listener: (...args: any[]) => void): this { return super.on(event, listener); } publish(data: BusEvent): void { this.emit("bus", data); } } export const eventBus = new EventBus(); eventBus.setMaxListeners(256); ``` ### jobs.ts — Payment-gated job lifecycle (first 120 lines) ```typescript import { Router, type Request, type Response } from "express"; import { randomUUID, createHash } from "crypto"; import { db, jobs, invoices, type Job } from "@workspace/db"; import { eq, and } from "drizzle-orm"; import { CreateJobBody, GetJobParams } from "@workspace/api-zod"; import { lnbitsService } from "../lib/lnbits.js"; import { agentService } from "../lib/agent.js"; import { pricingService } from "../lib/pricing.js"; import { jobsLimiter } from "../lib/rate-limiter.js"; import { eventBus } from "../lib/event-bus.js"; import { streamRegistry } from "../lib/stream-registry.js"; import { makeLogger } from "../lib/logger.js"; import { latencyHistogram } from "../lib/histogram.js"; import { trustService } from "../lib/trust.js"; import { freeTierService } from "../lib/free-tier.js"; import { zapService } from "../lib/zap.js"; const logger = makeLogger("jobs"); const router = Router(); async function getJobById(id: string): Promise { const rows = await db.select().from(jobs).where(eq(jobs.id, id)).limit(1); return rows[0] ?? null; } async function getInvoiceById(id: string) { const rows = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1); return rows[0] ?? null; } /** * Runs the AI eval in a background task (fire-and-forget) so HTTP polls * return immediately with "evaluating" state instead of blocking 5-8 seconds. * nostrPubkey is used for free-tier routing and trust scoring. */ async function runEvalInBackground( jobId: string, request: string, nostrPubkey: string | null, ): Promise { const evalStart = Date.now(); try { const evalResult = await agentService.evaluateRequest(request); latencyHistogram.record("eval_phase", Date.now() - evalStart); logger.info("eval result", { jobId, accepted: evalResult.accepted, reason: evalResult.reason, inputTokens: evalResult.inputTokens, outputTokens: evalResult.outputTokens, }); if (evalResult.accepted) { const { estimatedInputTokens, estimatedOutputTokens } = pricingService.estimateRequestCost(request, agentService.workModel); const breakdown = await pricingService.calculateWorkFeeSats( estimatedInputTokens, estimatedOutputTokens, agentService.workModel, ); // ── Free-tier gate ────────────────────────────────────────────────── const ftDecision = await freeTierService.decide(nostrPubkey, breakdown.amountSats); if (ftDecision.serve === "free") { // Pool was atomically debited for ftDecision.absorbSats by decide(). // Store ONLY the actual debited amount (not the estimate) so reconciliation // in recordGrant() can return over-reservation accurately. const reservedAbsorbed = ftDecision.absorbSats; // actual pool debit try { await db .update(jobs) .set({ state: "executing", workAmountSats: 0, estimatedCostUsd: breakdown.estimatedCostUsd, marginPct: breakdown.marginPct, btcPriceUsd: breakdown.btcPriceUsd, freeTier: true, absorbedSats: reservedAbsorbed, updatedAt: new Date(), }) .where(eq(jobs.id, jobId)); eventBus.publish({ type: "job:state", jobId, state: "executing" }); // Grant is recorded AFTER work completes (in runWorkInBackground) so we use // actual cost rather than estimated sats for the audit log. streamRegistry.register(jobId); setImmediate(() => { void runWorkInBackground( jobId, request, 0, breakdown.btcPriceUsd, true, nostrPubkey, reservedAbsorbed, // actual debited; runWorkInBackground will reconcile with actual cost ); }); } catch (setupErr) { // If DB transition or setup fails after pool was already debited, return sats. void freeTierService.releaseReservation( reservedAbsorbed, `free job setup failed: ${setupErr instanceof Error ? setupErr.message : String(setupErr)}`, ); throw setupErr; // re-throw so outer catch handles job state } return; } // Partial subsidy or full gate: invoice amount = chargeSats const invoiceSats = ftDecision.serve === "partial" ? ftDecision.chargeSats : breakdown.amountSats; const workInvoiceData = await lnbitsService.createInvoice( invoiceSats, `Work fee for job ${jobId}`, ); const workInvoiceId = randomUUID(); await db.transaction(async (tx) => { await tx.insert(invoices).values({ … (721 more lines truncated) ``` ### moderation.ts — Nostr relay moderation queue + Timmy AI review ```typescript /** * moderation.ts — Event moderation queue + Timmy AI review * * Every Nostr event from a non-elite whitelisted account is held in * relay_event_queue with status "pending". Timmy (Claude haiku) reviews * pending events in a background poll loop and either auto_approves them * (injecting into strfry) or flags them for admin review. * * Elite accounts bypass this queue — their events are injected directly * from the relay policy handler. */ import { db, relayEventQueue, type QueueReviewer } from "@workspace/db"; import { eq, and } from "drizzle-orm"; import { makeLogger } from "./logger.js"; import { injectEvent } from "./strfry.js"; const logger = makeLogger("moderation"); // ── Stub mode (mirrors agent.ts) ───────────────────────────────────────────── const STUB_MODE = !process.env["AI_INTEGRATIONS_ANTHROPIC_API_KEY"] || !process.env["AI_INTEGRATIONS_ANTHROPIC_BASE_URL"]; if (STUB_MODE) { logger.warn("no Anthropic key — moderation running in STUB mode (auto-approve all)"); } // ── Anthropic lazy client (reuse from agent.ts pattern) ────────────────────── interface AnthropicLike { messages: { create(params: Record): Promise<{ content: Array<{ type: string; text?: string }>; usage: { input_tokens: number; output_tokens: number }; }>; }; } let _anthropic: AnthropicLike | null = null; async function getClient(): Promise { if (_anthropic) return _anthropic; // @ts-expect-error -- integrations-anthropic-ai exports src directly const mod = (await import("@workspace/integrations-anthropic-ai")) as { anthropic: AnthropicLike }; _anthropic = mod.anthropic; return _anthropic; } // ── Moderation prompt ───────────────────────────────────────────────────────── const MODERATION_SYSTEM = `You are moderating events on a sovereign Nostr relay. Your job is to approve benign content and flag anything harmful. APPROVE if the event is: a standard text note, profile update, reaction, encrypted DM, relay list, metadata update, or other typical Nostr activity. FLAG if the event is: spam, harassment, illegal content, NSFW without appropriate warnings, coordinated abuse, or clearly malicious. Respond ONLY with valid JSON: {"decision": "approve", "reason": "..."} or {"decision": "flag", "reason": "..."}`; type ModerationDecision = "approve" | "flag"; interface ModerationResult { decision: ModerationDecision; reason: string; } async function callClaude(kind: number, content: string): Promise { if (STUB_MODE) { return { decision: "approve", reason: "Stub: auto-approved (no Anthropic key)" }; } const client = await getClient(); const message = await client.messages.create({ model: process.env["MODERATION_MODEL"] ?? "claude-haiku-4-5", max_tokens: 256, system: MODERATION_SYSTEM, messages: [ { role: "user", content: `Nostr event kind ${kind}. Content: ${content.slice(0, 2000)}`, }, ], }); const block = message.content[0]; if (!block || block.type !== "text") { return { decision: "flag", reason: "AI returned unexpected response" }; } try { const raw = block.text!.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim(); const parsed = JSON.parse(raw) as { decision: string; reason?: string }; const decision = parsed.decision === "approve" ? "approve" : "flag"; return { decision, reason: parsed.reason ?? "" }; } catch { logger.warn("moderation: failed to parse Claude response", { text: block.text!.slice(0, 100), }); return { decision: "flag", reason: "Failed to parse AI response" }; } } // ── ModerationService ───────────────────────────────────────────────────────── export class ModerationService { /** * Insert an event into the moderation queue with "pending" status. * Idempotent: if the event_id already exists, the insert is silently skipped. */ async enqueue(event: { id: string; pubkey: string; kind: number; rawJson: string; }): Promise { await db .insert(relayEventQueue) .values({ eventId: event.id, pubkey: event.pubkey, … (149 more lines truncated) ``` ### world-state.ts — In-memory Timmy state + agent mood derivation ```typescript export interface TimmyState { mood: string; activity: string; } export interface WorldState { timmyState: TimmyState; agentStates: Record; updatedAt: string; } const DEFAULT_TIMMY: TimmyState = { mood: "contemplative", activity: "idle", }; const _state: WorldState = { timmyState: { ...DEFAULT_TIMMY }, agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle" }, updatedAt: new Date().toISOString(), }; export function getWorldState(): WorldState { return { timmyState: { ..._state.timmyState }, agentStates: { ..._state.agentStates }, updatedAt: _state.updatedAt, }; } export function setAgentStateInWorld(agentId: string, agentState: string): void { _state.agentStates[agentId] = agentState; _state.updatedAt = new Date().toISOString(); _deriveTimmy(); } function _deriveTimmy(): void { const states = Object.values(_state.agentStates); if (states.includes("working")) { _state.timmyState.activity = "working"; _state.timmyState.mood = "focused"; } else if (states.includes("thinking") || states.includes("evaluating")) { _state.timmyState.activity = "thinking"; _state.timmyState.mood = "curious"; } else if (states.some((s) => s !== "idle")) { _state.timmyState.activity = "active"; _state.timmyState.mood = "attentive"; } else { _state.timmyState.activity = "idle"; _state.timmyState.mood = "contemplative"; } } ``` --- ## Key architectural facts for context - Every external dependency has a **stub mode**: LNbits (in-memory invoices), Anthropic AI (canned responses), Digital Ocean (fake credentials + real SSH keypair). - Env-var tunable constants follow a consistent pattern: `envInt("VAR_NAME", defaultValue)`. - Service classes have a singleton export at the bottom of the file. - All routes use `makeLogger` structured logger and `@workspace/db` Drizzle ORM. - The `eventBus` pub/sub decouples state transitions from WebSocket broadcast. - Job state machine: awaiting_eval_payment → evaluating → awaiting_work_payment → executing → complete/rejected/failed. - Trust tiers: new → established (10pts) → trusted (50pts) → elite (200pts). Soft decay after 30 days absent. - Pre-funded session mode (Mode 2): pay once, debit at actual cost, no per-job invoices. - Testkit: 36 automated tests at GET /api/testkit (returns a self-contained bash script). --- *Generated by `pnpm --filter @workspace/scripts timmy-report` on 2026-03-19.*