This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
token-gated-economy/reports/context.md
alexpaynex 1a268353f9 Update report generation to dynamically discover and display author commit data
Refactor `timmy-report.ts` to dynamically collect and display author commit samples from git log, update `context.md` to reflect dynamic author data, and adjust `timmy-report.md` to use the new dynamic contributor summary.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 90c7a60b-2c61-4699-b5c6-6a1ac7469a4d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cf2341e4-4927-4087-a7c9-a93340626de0
Replit-Helium-Checkpoint-Created: true
2026-03-19 23:54:15 +00:00

36 KiB
Raw Permalink Blame History

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 T25T36 — Nostr identity + trust engine coverage (v2)
c7bb5de task/35: Testkit T25T36 — 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

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<NostrIdentity> {
    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<NostrIdentity | null> {
    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<TrustTier> {
    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

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)

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<Job | null> {
  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<void> {
  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

/**
 * 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<string, unknown>): Promise<{
      content: Array<{ type: string; text?: string }>;
      usage: { input_tokens: number; output_tokens: number };
    }>;
  };
}

let _anthropic: AnthropicLike | null = null;

async function getClient(): Promise<AnthropicLike> {
  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<ModerationResult> {
  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<void> {
    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

export interface TimmyState {
  mood: string;
  activity: string;
}

export interface WorldState {
  timmyState: TimmyState;
  agentStates: Record<string, string>;
  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.