Commit Graph

42 Commits

Author SHA1 Message Date
0bc4c6f825 [gemini] Implement Lightning-Gated Node Bootstrap feature (#50) (#98) 2026-03-23 21:28:35 +00:00
2ad3403061 [claude] Agent commentary during job execution (#1) (#94) 2026-03-23 20:41:57 +00:00
5dc71e1257 [claude] API observability — structured logging + /api/metrics endpoint (#57) (#87) 2026-03-23 20:10:40 +00:00
821aa48543 [claude] Add real-time cost ticker for Workshop interactions (#68) (#82)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-23 20:01:26 +00:00
4ea59f7198 [claude] Context injection — pass conversation history to work model (#39) (#78) 2026-03-23 01:51:22 +00:00
609acc8f66 [claude] Agent debate on borderline eval requests (#21) (#72) 2026-03-23 01:07:52 +00:00
5954a2fdc0 [claude] Fix moderation infinite re-review loop (#27) (#71) 2026-03-23 00:44:57 +00:00
alexpaynex
1f22aa61bc feat(agent): Timmy anti-walled-garden persona (Task #43)
Baked Timmy's anti-walled-garden, open-source identity into all AI system
prompts across artifacts/api-server/src/lib/agent.ts and engagement.ts.

Changes:
1. chatReply prompt — Extended wizard persona to include open-source ethos
   ("AI Johnny Appleseed", seeds scattered freely, not walled gardens).
   Added an explicit EXCEPTION section for self-hosting requests — brevity
   rule is suspended and a full practical rundown is given (pnpm monorepo,
   stack, env vars, startup command). No hedging, no upselling hosted version.
   Also bumped max_tokens from 150 → 400 so self-hosting replies are not
   hard-truncated, and removed the 250-char slice() from the return value.

2. executeWork and executeWorkStreaming prompts — Same open ethos and full
   self-hosting reference added so paid job self-hosting requests get
   identical, accurate guidance. Both prompts are kept in sync.

3. evaluateRequest prompt — Added an explicit ALWAYS ACCEPT rule for
   self-hosting, open-source, and "how do I run this myself" requests so they
   are never treated as edge cases or rejected.

4. Nostr outreach prompt (engagement.ts) — Lightly updated to include
   Timmy's open/self-sovereign identity and allow optional open-source mention
   when it fits naturally; tone stays warm and non-pushy.

No UI changes, no schema changes, no payment logic touched.

Replit-Task-Id: 4a4a7219-88aa-4a4e-8490-6f7c17e8adfb
2026-03-20 00:02:52 +00:00
alexpaynex
a95fd76ebd task/32: Event moderation queue + Timmy AI review
## What was built
Full moderation pipeline: relay_event_queue table, strfry inject helper,
ModerationService with Claude haiku review, policy tier routing, 30s poll loop,
admin approve/reject/list endpoints.

## DB schema (`lib/db/src/schema/relay-event-queue.ts`)
relay_event_queue: event_id (PK), pubkey (FK → nostr_identities), kind,
raw_event (text JSON), status (pending/approved/rejected/auto_approved),
reviewed_by (timmy_ai/admin/null), review_reason, created_at, decided_at.
Exported from schema/index.ts. Pushed via pnpm run push.

## strfry HTTP client (`artifacts/api-server/src/lib/strfry.ts`)
injectEvent(rawEventJson) — POST {STRFRY_URL}/import (NDJSON).
STRFRY_URL defaults to "http://strfry:7777" (Docker internal network).
5s timeout; graceful failure in dev when strfry not running; never throws.

## ModerationService (`artifacts/api-server/src/lib/moderation.ts`)
- enqueue(event) — insert pending row; idempotent onConflictDoNothing
- autoReview(eventId) — Claude haiku prompt: approve or flag. On flag, marks
  reviewedBy=timmy_ai and leaves pending for admin. On approve, calls decide().
- decide(eventId, status, reason, reviewedBy) — updates DB + calls injectEvent
- processPending(limit=10) — batch poll: auto-review up to limit pending events
- Stub mode: auto-approves all events when Anthropic key absent

## Policy endpoint update (`artifacts/api-server/src/routes/relay.ts`)
Tier routing in evaluatePolicy:
  read/none → reject (unchanged)
  write + elite tier → injectEvent + accept (elite bypass; shadowReject if inject fails)
  write + non-elite → enqueue + shadowReject (held for moderation)
Imports db/nostrIdentities directly for tier check. Both inject and enqueue errors
are fail-closed (reject vs shadowReject respectively).

## Background poll loop (`artifacts/api-server/src/index.ts`)
setInterval every 30s calling moderationService.processPending(10).
Interval configurable via MODERATION_POLL_MS env var.
Errors caught per-event; poll loop never crashes the server.

## Admin queue routes (`artifacts/api-server/src/routes/admin-relay-queue.ts`)
ADMIN_SECRET Bearer auth (same pattern as admin-relay.ts).
GET  /api/admin/relay/queue?status=...        — list all / by status
POST /api/admin/relay/queue/:eventId/approve  — approve + inject into strfry
POST /api/admin/relay/queue/:eventId/reject   — reject (no inject)
409 on duplicate decisions. Registered in routes/index.ts.

## Smoke tests (all pass)
Unknown → reject ✓; elite → shadowReject (strfry unavailable in dev) ✓;
non-elite write → shadowReject + pending in queue ✓; admin approve → approved ✓;
moderation poll loop started ✓; TypeScript 0 errors.
2026-03-19 20:35:39 +00:00
alexpaynex
01374375fb Update default access for new accounts to read-only
Modify the default access level for newly created accounts from "none" to "read" and clarify access semantics in relay-accounts.ts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0a15bba0-45a8-4d39-960b-683e2568bd77
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
2026-03-19 20:29:02 +00:00
alexpaynex
31a843a829 task/31: Relay account whitelist + trust-gated access (v2 — code review fixes)
## What was built
Full relay access control: relay_accounts table, RelayAccountService,
trust hook, live policy enforcement, admin CRUD API, elite startup seed.

## DB schema (`lib/db/src/schema/relay-accounts.ts`)
relay_accounts table: pubkey (PK, FK nostr_identities ON DELETE CASCADE),
access_level (none/read/write), granted_by (text), granted_at, revoked_at, notes.
Exported from lib/db/src/schema/index.ts. Pushed via pnpm run push.

## RelayAccountService (`artifacts/api-server/src/lib/relay-accounts.ts`)
- getAccess(pubkey) → RelayAccessLevel (none if missing or revoked)
- grant(pubkey, level, reason, grantedBy) — upsert; creates nostr_identity FK
- revoke(pubkey, reason) — sets revokedAt, accessLevel→none, grantedBy→"manual-revoked"
  The "manual-revoked" marker prevents syncFromTrustTier from auto-reinstating.
  Only explicit admin grant() can restore access after revocation.
- syncFromTrustTier(pubkey) — fetches tier from DB internally (no tier param to
  avoid caller drift). Respects: manual-revoked (skip), manual active (upgrade only),
  auto-tier (full sync). Never auto-reinstates revoked accounts.
- seedElite(pubkey, notes) — upserts nostr_identities with tier="elite" +
  trustScore=200, then grants relay write access as a permanent manual grant.
  Called at startup for Timmy's own pubkey.
- list(opts) — returns all accounts, filtered by activeOnly if requested.
- Tier→access: new=none, established/trusted/elite=write (env-overridable)

## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier(pubkey) after DB write.
Fire-and-forget with catch (trust flow is never blocked by relay errors).

## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() async: queries relay_accounts.getAccess(). write→accept,
read/none/missing→reject. DB error→reject (fail-closed).

## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer auth (localhost-only fallback in dev; error log in prod).
GET  /api/admin/relay/accounts             — list all accounts
POST /api/admin/relay/accounts/:pk/grant   — grant (level + notes)
POST /api/admin/relay/accounts/:pk/revoke  — revoke (sets manual-revoked)
pubkey validation: 64-char lowercase hex only.

## Startup seed (`artifacts/api-server/src/index.ts`)
Resolves pubkey from TIMMY_NOSTR_PUBKEY env first, falls back to
timmyIdentityService.pubkeyHex. Calls seedElite() — idempotent upsert.
Sets nostr_identities.tier="elite" alongside relay write access.

## Smoke test results (all pass)
Timmy accept ✓; unknown reject ✓; grant→accept ✓; revoke→manual-revoked ✓;
revoked stays rejected ✓; TypeScript 0 errors.
2026-03-19 20:26:03 +00:00
alexpaynex
94613019fc task/31: Relay account whitelist + trust-gated access
## What was built
Full relay access control system: relay_accounts table, RelayAccountService,
trust hook integration, live policy enforcement, admin CRUD API, Timmy seed.

## DB change
`lib/db/src/schema/relay-accounts.ts` — new `relay_accounts` table:
  pubkey (PK, FK → nostr_identities.pubkey ON DELETE CASCADE),
  access_level ("none"|"read"|"write"), granted_by ("manual"|"auto-tier"),
  granted_at, revoked_at (nullable), notes. Pushed via `pnpm run push`.
`lib/db/src/schema/index.ts` — exports relay-accounts.

## RelayAccountService (`artifacts/api-server/src/lib/relay-accounts.ts`)
- getAccess(pubkey) → RelayAccessLevel (none if missing or revoked)
- grant(pubkey, level, reason, grantedBy) — upsert; creates nostr_identity FK
- revoke(pubkey, reason) — sets revokedAt, access_level → none
- syncFromTrustTier(pubkey, tier) — auto-promotes by tier; never downgrades manual grants
- list(opts) — returns all accounts, optionally filtered to active
- Tier→access map: new=none, established/trusted/elite=write (env-overridable)

## Trust hook (`artifacts/api-server/src/lib/trust.ts`)
recordSuccess + recordFailure both call syncFromTrustTier after writing tier.
Failure is caught + logged (non-blocking — trust flow never fails on relay error).

## Policy endpoint (`artifacts/api-server/src/routes/relay.ts`)
evaluatePolicy() now async: queries relay_accounts.getAccess(pubkey).
"write" → accept; "read"/"none"/missing → reject with clear message.
DB error → reject with "policy service error" (safe fail-closed).

## Admin routes (`artifacts/api-server/src/routes/admin-relay.ts`)
ADMIN_SECRET Bearer token auth (localhost-only fallback in dev; error log in prod).
GET  /api/admin/relay/accounts            — list all accounts
POST /api/admin/relay/accounts/:pk/grant  — grant (level + notes body)
POST /api/admin/relay/accounts/:pk/revoke — revoke (reason body)
pubkey validation: must be 64-char lowercase hex.

## Startup seed (`artifacts/api-server/src/index.ts`)
On every startup: grants Timmy's own pubkeyHex "write" access ("manual").
Idempotent upsert — safe across restarts.

## Smoke test results (all pass)
- Timmy pubkey → accept ✓; unknown pubkey → reject ✓
- Admin grant → accept ✓; admin revoke → reject ✓; admin list shows accounts ✓
- TypeScript: 0 errors in API server + lib/db
2026-03-19 20:21:12 +00:00
alexpaynex
0b3a701933 Add security measures to prevent malicious requests when fetching LNURL data
Implement SSRF protection by validating URLs, blocking private IPs, and enforcing HTTPS in the LNURL fetch path.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 35635246-7119-4788-b55f-66a002409788
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu
Replit-Helium-Checkpoint-Created: true
2026-03-19 19:55:59 +00:00
alexpaynex
33b47f8682 task/29: fix code review findings — LNURL zap, vouch binding, migration SQL
## Code review issues resolved

### 1. Zap-out: real LNURL-pay resolution (was: log-only when no bolt11)
- `zap.ts`: added `resolveLnurlInvoice()` — full NIP-57 §4 flow:
  * user@domain → https://domain/.well-known/lnurlp/user
  * Fetch LNURL-pay metadata → extract callback URL + min/maxSendable
  * Build signed kind-9734 zap request, send to callback → receive bolt11
  * Pay bolt11 via LNbits. Log event regardless of payment outcome.
- `nostr-identities.ts`: added `lightningAddress` column (nullable TEXT)
- `identity.ts /verify`: extracts `["lud16", "user@domain.com"]` tag from
  signed event and stores it so ZapService can resolve future invoices
- `maybeZapOnJobComplete()` now triggers real payment when lightningAddress
  is stored; logs a warning and skips payment if not available

### 2. Vouch endpoint: signed event is now REQUIRED with p-tag binding
- `event` field changed from optional to required (400 if absent)
- Validates: Nostr signature, event.pubkey matches authenticated voucher
- NEW: event MUST contain a `["p", voucheePubkey]` tag — proves the voucher
  intentionally named the vouchee in their signed event (co-signature binding)

### 3. DB migration file added
- `lib/db/migrations/0006_timmy_economic_peer.sql` — covers:
  * CREATE TABLE IF NOT EXISTS timmy_nostr_events (with indexes)
  * CREATE TABLE IF NOT EXISTS nostr_trust_vouches (with indexes)
  * ALTER TABLE nostr_identities ADD COLUMN IF NOT EXISTS lightning_address
- Schema pushed to production: `lightning_address` column confirmed live

### Additional
- `GET /api/identity/timmy` now includes `relayUrl` field (null when unset)
- TypeScript compiles cleanly (tsc --noEmit: 0 errors)
- All smoke tests pass: /timmy 200, /challenge nonce, /vouch 401/400
2026-03-19 19:47:00 +00:00
Replit Agent
eb5dcfd48a task-29: Timmy as economic peer — Nostr identity, zap-out, vouching, engagement
1. TimmyIdentityService (artifacts/api-server/src/lib/timmy-identity.ts)
   - Loads nsec from TIMMY_NOSTR_NSEC env var at boot (bech32 decode)
   - Generates and warns about ephemeral key if env var absent
   - sign(EventTemplate) → finalizeEvent() with Timmy's key
   - encryptDm(recipientPubkeyHex, plaintext) → NIP-04 nip04.encrypt()
   - Logs npub at server startup

2. ZapService (artifacts/api-server/src/lib/zap.ts)
   - Constructs NIP-57 zap request event (kind 9734), signs with Timmy's key
   - Pays via lnbitsService.payInvoice() if bolt11 provided (stub-mode aware)
   - Logs every outbound event to timmy_nostr_events audit table
   - maybeZapOnJobComplete() wired in jobs.ts after trustService.recordSuccess()
   - Config: ZAP_PCT_DEFAULT (default 0 = disabled), ZAP_MIN_SATS (default 10)
   - Only fires for trusted/elite tier partners when ZAP_PCT_DEFAULT > 0

3. Engagement engine (artifacts/api-server/src/lib/engagement.ts)
   - Configurable cadence: ENGAGEMENT_INTERVAL_DAYS (default 0 = disabled)
   - Queries nostrIdentities for trustScore >= 50 AND lastSeen < threshold
   - Generates personalised DM via agentService.chatReply()
   - Encrypts as NIP-04 DM (kind 4), signs with Timmy's key
   - Logs to timmy_nostr_events; publishes to NOSTR_RELAY_URL if set
   - First run delayed 60s after startup to avoid cold-start noise

4. Vouching endpoint (artifacts/api-server/src/routes/identity.ts)
   - POST /api/identity/vouch: requires X-Nostr-Token with elite tier
   - Verifies optional Nostr event signature from voucher
   - Records relationship in nostr_trust_vouches table
   - Applies VOUCH_TRUST_BOOST (20 pts) to vouchee's trust score
   - GET /api/identity/timmy: public endpoint returning npub + zap count

5. DB schema additions (lib/db/src/schema/)
   - timmy_nostr_events: audit log for all outbound Nostr events
   - nostr_trust_vouches: voucher/vouchee social graph with boost amount
   - Tables created in production DB via drizzle-kit push

6. Frontend identity card (the-matrix/)
   - #timmy-id-card: fixed bottom-right widget with Timmy's npub + zap count
   - timmy-id.js: initTimmyId() fetches /api/identity/timmy on load
   - Npub shortened (npub1xxxx...yyyyyy), click-to-copy with feedback
   - Refreshes every 60s to show live zap count
   - Wired into main.js on firstInit
2026-03-19 19:27:13 +00:00
alexpaynex
599771e0ae Task #27: Atomic free-tier gate — complete, all reviewer issues fixed
== All fixed issues ==

1. /api/estimate pool mutation (fixed)
   - Added decideDryRun(): non-mutating read-only free-tier preview
   - /api/estimate uses decideDryRun(); pool never debited by estimate calls

2. Free-path passes actual debited amount not estimate (fixed)
   - In runEvalInBackground free path: uses ftDecision.absorbSats (actual pool debit)
   - DB absorbedSats column set to actual debited sats, not breakdown.amountSats
   - runWorkInBackground receives reservedAbsorbed = actual pool debit

3. decide() free branch: downgrade to partial if atomic debit < estimated (fixed)
   - After _atomicPoolDebit, if debited < estimatedSats (pool raced):
     - Release the partial debit back to pool
     - Return serve="partial" with advisory amounts (re-reserved at payment time)
   - Only returns serve="free" with chargeSats=0 if debited >= estimatedSats

4. Reservation leak on pre-work failure (fixed)
   - Free path wrapped in inner try/catch around DB update + setImmediate
   - If setup fails after pool debit: releaseReservation() called; throws so outer
     catch sets job to failed state

5. Partial-job pool-drained at payment => fail with refund (implemented)
   - reservePartialGrant() = 0 at payment time => job.state = failed
   - refundState = pending, refundAmountSats = workAmountSats (user gets money back)
   - Work does NOT execute under discounted terms without pool backing

6. Partial-job refund math corrected (fixed)
   - actualUserChargeSats = max(0, actualTotalCostSats - partialAbsorbSats)
   - refund = workAmountSats - actualUserChargeSats

7. Grant audit reconciliation (fixed)
   - actualAbsorbed = min(actualTotalCostSats, reservedAbsorbed)
   - over-reservation returned to pool atomically in recordGrant()
   - Audit log and daily counter reflect actual absorbed sats

New API: decideDryRun(), reservePartialGrant(), releaseReservation()
New recordGrant signature: (pubkey, hash, actualAbsorbed, reservedAbsorbed)
2026-03-19 17:25:13 +00:00
alexpaynex
eca505e47e Task #27: Atomic free-tier gate — complete fix of all reviewer-identified issues
== Issue 1: /api/estimate was mutating pool state (fixed) ==
Added decideDryRun() to FreeTierService — non-mutating read-only preview that
reads pool/trust state but does NOT debit the pool or reserve anything.
/api/estimate now calls decideDryRun() instead of decide().
Pool and daily budgets are never affected by estimate calls.

== Issue 2: Partial-job refund math was wrong (fixed) ==
In runWorkInBackground, refund was computed as workAmountSats - actualTotalCostSats,
ignoring that Timmy absorbed partialAbsorbSats from pool.
Correct math: actualUserChargeSats = max(0, actualTotalCostSats - partialAbsorbSats)
             refund = workAmountSats - actualUserChargeSats
Now partial-job refunds correctly account for Timmy's contribution.

== Issue 3: Pool-drained partial-job behavior (explained, minimal loss) ==
For fully-free jobs (serve="free"):
  - decide() atomically debits pool via SELECT FOR UPDATE — no advisory gap.
  - Pool drained => decide() returns gate => work does not start. ✓

For partial jobs (serve="partial"):
  - decide() is advisory; pool debit deferred to reservePartialGrant() at
    payment confirmation in advanceJob().
  - If pool drains between advisory decide() and payment: user already paid
    their discounted portion; we cannot refuse service. Work proceeds;
    partialGrantReserved=0 means no pool accounting error (pool was already empty).
  - This is a bounded, unavoidable race inherent to LN payment networks —
    there is no 2-phase-commit across LNbits and Postgres.
  - "Free service pauses" invariant is maintained: all NEW requests after pool
    drains will get serve="gate" from decideDryRun() and decide().

== Audit log accuracy (fixed in prior commit, confirmed) ==
recordGrant(pubkey, hash, actualAbsorbed, reservedAbsorbed):
  - actualAbsorbed = min(actualTotalCostSats, reservedAbsorbed)
  - over-reservation (estimated > actual) returned to pool atomically
  - daily counter and audit log reflect actual absorbed sats
2026-03-19 17:17:54 +00:00
alexpaynex
ec5316a4dc Task #27: Atomic free-tier pool reservation — eliminates advisory-charge gap
Root cause: decide() was advisory but user charges were reduced from its output;
recordGrant() later might absorb less, so Timmy could absorb the gap silently.

Fix architecture (serve="free" path — fully-free jobs + sessions):
  - decide() now runs _atomicPoolDebit() inside a FOR UPDATE transaction
  - Pool is debited at decision time for serve="free" decisions
  - Work starts immediately after, so no window for pool drain between debit and use
  - If work fails → releaseReservation() returns sats to pool

Fix architecture (serve="partial" path — partial-subsidy jobs):
  - decide() remains advisory for "partial" (no pool debit at decision time)
  - This prevents pool drain from users who get a partial offer but never pay
  - For jobs: reservePartialGrant() atomically debits pool at work-payment-confirmation
    time (inside advanceJob), before work begins
  - For sessions: reservePartialGrant() called after synchronous work completes,
    using actual cost capped by advisory absorbSats

recordGrant() now takes (pubkey, requestHash, actualAbsorbed, reservedAbsorbed):
  - Over-reservation (estimated > actual) returned to pool atomically
  - Audit log and daily counter reflect actual absorbed amount
  - Pool balance was already decremented by decide() or reservePartialGrant()

Result: In ALL paths, pool debit happens atomically before charges are reduced.
User charge reduction and pool debit are always consistent — Timmy never operates
at a loss due to concurrent pool depletion.
2026-03-19 17:08:43 +00:00
alexpaynex
373477ba7f Task #27: Complete cost-routing + free-tier gate — all critical fixes applied
Fix 1 — Add `estimateRequestCost(request, model)` to PricingService (pricing.ts)
  - Unified: estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd
  - Replaces duplicated estimation in jobs.ts, sessions.ts, estimate.ts

Fix 2 — Sessions pre-gate: estimate → decide → execute → reconcile
  - freeTierService.decide() runs on ESTIMATED cost BEFORE executeWork()
  - Fixed double-margin: estimateRequestCost already includes infra+margin; convert directly
  - absorbedSats capped at actual cost post-execution (Math.min)

Fix 3 — Correct isFree derivation for partial jobs in advanceJob() (jobs.ts)
  - isFreeExecution = workAmountSats === 0 (not job.freeTier)
  - Partial jobs run paid accounting: actual sats, refund, pool credit, deferred grant

Fix 4 — Defer ALL grant recording to post-work execution (jobs.ts)
  - Fully-free path: removed recordGrant from eval time; now called in runWorkInBackground
  - For isFree jobs: absorbCap = actual post-execution cost (calculateActualChargeSats)
  - For partial jobs: grant deferred from invoice creation to after work completes

Fix 5 — Atomic, pool-bounded grant recording with row locking (free-tier.ts)
  - SELECT ... FOR UPDATE locks pool row inside transaction
  - actualAbsorbed = Math.min(absorbSats, poolBalance) — pool can never go negative
  - Pool balance update is plain write (lock already held)
  - Daily absorption: SQL CASE expression atomically handles new-day reset
  - Audit log and identity counter both reflect actualAbsorbed, not requested amount
  - If pool is empty at grant time, transaction returns without writing

Fix 6 — Remove fire-and-forget (void) from all recordGrant() call sites
  - All three call sites now use await; grant failures propagate correctly
  - Removed unused createHash import from free-tier.ts
2026-03-19 16:59:11 +00:00
alexpaynex
1754ab1dbc Task #27: Complete cost-routing + free-tier gate — all critical fixes applied
Fix 1 — Add `estimateRequestCost(request, model)` to PricingService (pricing.ts)
  - Unified: estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd
  - Replaces duplicated estimation logic in jobs.ts, sessions.ts, estimate.ts

Fix 2 — Move partial free-tier `recordGrant()` from invoice creation to post-work
  - Was called at invoice creation — economic DoS vulnerability
  - Now deferred to runWorkInBackground via new `partialAbsorbSats` param
  - Fully-free jobs still record grant at eval time (no payment involved)

Fix 3 — Sessions pre-gate: estimate → decide → execute → reconcile
  - freeTierService.decide() now runs on ESTIMATED cost BEFORE executeWork()
  - Fixed double-margin bug: estimateRequestCost returns cost already with infra+margin
    (calculateWorkFeeUsd), convert directly to sats — no second calculateActualChargeUsd
  - absorbedSats capped at actual cost post-execution to prevent over-absorption

Fix 4 — Correct isFree derivation for partial jobs in advanceJob() (jobs.ts)
  - isFreeExecution = workAmountSats === 0 (not job.freeTier)
  - Partial jobs (freeTier=true, workAmountSats>0) run the paid accounting path:
    actual sats, refund eligibility, pool credit, and deferred grant recording

Fix 5 — Atomic pool deduction + daily absorption in recordGrant (free-tier.ts)
  - Pool: SQL GREATEST(value::int - N, 0)::text inside transaction, RETURNING actual value
  - Daily absorption: SQL CASE expression checks absorbed_reset_at age in DB
    → reset counter on new day, increment atomically otherwise
  - No more application-layer read-modify-write for either counter

Fix 6 — Remove fire-and-forget from all recordGrant() call sites
  - Removed `void` prefix from all three call sites (jobs.ts x2, sessions.ts x1)
  - Grant persistence failures now propagate correctly instead of silently diverging
  - Removed unused createHash import from free-tier.ts
2026-03-19 16:55:03 +00:00
alexpaynex
3a617669f0 Task #27: Apply 3 required fixes for cost-routing + free-tier gate
1. Add `estimateRequestCost(request, model)` to PricingService in pricing.ts
   - Unified method combining estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd
   - Replaces duplicated token estimation logic at call sites in jobs.ts, sessions.ts, estimate.ts

2. Move partial free-tier `recordGrant()` from invoice creation to post-work in runWorkInBackground
   - Previously called at invoice creation for partial path — economic DoS vulnerability
   - Now deferred to after work completes via new `partialAbsorbSats` param in runWorkInBackground
   - Fully-free jobs still record grant at eval time (no payment involved)

3. Sessions pre-gate: estimate → decide → execute → reconcile (with double-margin bug fix)
   - Free-tier `decide()` now runs on ESTIMATED cost BEFORE `executeWork()` is called
   - Fixed: estimateRequestCost already includes infra+margin via calculateWorkFeeUsd,
     so convert estimatedCostUsd directly to sats — no second calculateActualChargeUsd call
   - absorbedSats capped at actual cost post-execution (Math.min) to prevent over-absorption

4. Atomic pool deduction in recordGrant (free-tier.ts)
   - Replaced non-atomic read-then-write pattern with SQL GREATEST expression inside transaction
   - UPDATE timmyConfig SET value = GREATEST(value::int - absorbSats, 0)::text RETURNING value
   - Audit log (freeTierGrants) receives actual post-deduct value from DB; no oversubscription
   - Removed unused createHash import from free-tier.ts
2026-03-19 16:47:51 +00:00
alexpaynex
512089ca08 Task #27: Apply 3 required fixes for cost-routing + free-tier gate
1. Add `estimateRequestCost(request, model)` to PricingService in pricing.ts
   - Unified method combining estimateInputTokens + estimateOutputTokens + calculateWorkFeeUsd
   - Replaces duplicated token estimation logic at call sites in jobs.ts, sessions.ts, estimate.ts

2. Move partial free-tier `recordGrant()` from invoice creation to post-work in runWorkInBackground
   - Previously called at invoice creation for partial path (before user pays) — economic DoS vulnerability
   - Now deferred to after work completes, using new `partialAbsorbSats` parameter in runWorkInBackground
   - Fully-free jobs still record grant at eval time (no payment involved)

3. Sessions pre-gate refactor: estimate → decide → execute → reconcile
   - Free-tier `decide()` now runs on ESTIMATED cost BEFORE `executeWork()` is called
   - After execution, `absorbedSats` is capped at actual cost (Math.min) to prevent over-absorption
   - Uses new `estimateRequestCost()` for clean single-call estimation
2026-03-19 16:43:41 +00:00
alexpaynex
4c3a0e867a Task #27: Cost-routing + free-tier gate
## What was built

### DB schema
- `timmy_config` table: key/value store for the generosity pool balance
- `free_tier_grants` table: immutable audit log of every Timmy-absorbed request
- `jobs.free_tier` (boolean) + `jobs.absorbed_sats` (integer) columns

### FreeTierService (`lib/free-tier.ts`)
- Per-tier daily sats budgets (new=0, established=50, trusted=200, elite=1000)
  — all env-var overridable
- `decide(pubkey, estimatedSats)` → `{ serve: free|partial|gate, absorbSats, chargeSats }`
  — checks pool balance AND identity daily budget atomically
- `credit(paidSats)` — credits POOL_CREDIT_PCT (default 10%) of every paid
  work invoice back to the generosity pool
- `recordGrant(pubkey, reqHash, absorbSats)` — DB transaction: deducts pool,
  updates identity daily absorption counter, writes audit row
- `poolStatus()` — snapshot for metrics/monitoring

### Route integration
- `POST /api/jobs` (eval → work flow): after eval passes, `freeTierService.decide()`
  intercepts. Free → skip invoice, fire work directly. Partial → discounted invoice.
  Gate (anonymous/new tier/pool empty) → unchanged full-price flow.
- `POST /api/sessions/:id/request`: after compute, free-tier discount applied to
  balance debit. Session balance only reduced by `chargeSats`; absorbed portion
  comes from pool.
- Pool credited on every paid work completion (both jobs and session paths).
- Response fields: `free_tier: true`, `absorbed_sats: N` when applicable.

### GET /api/estimate
- Lightweight pre-flight cost estimator; no payment required
- Returns: estimatedSats, btcPriceUsd, tokenEstimate, identity.free_tier decision
  (if valid nostr_token provided), pool.balanceSats, pool.dailyBudgets

### Tests
- All 29 existing testkit tests pass (0 failures)
- Anonymous/new-tier users hit gate path correctly (verified manually)
- Pool seeds to 10,000 sats on first boot

## Architecture notes
- Free tier decision happens BEFORE invoice creation for jobs (save user the click)
- Partial grant recorded at invoice creation time (reserves pool capacity proactively)
- Free tier for sessions decided AFTER compute (actual cost known, applied to debit)
- Pool crediting is fire-and-forget (non-blocking)
2026-03-19 16:34:05 +00:00
Replit Agent
b0ac398cf2 fix(#26): apply decay before score mutations in recordSuccess/recordFailure
Previously recordSuccess/recordFailure read identity.trustScore (raw stored value)
and incremented/decremented from there. Long-absent identities could instantly
recover their pre-absence tier on the first interaction, defeating decay.

Fix: both methods now call applyDecay(identity) first to get the true current
baseline, then apply the score delta from there before persisting.
2026-03-19 16:11:36 +00:00
Replit Agent
9b778351e4 feat(#26): Nostr identity + trust engine
- New nostr_identities DB table (pubkey, trust_score, tier, interaction_count, sats_absorbed_today, last_seen)
- nullable nostr_pubkey FK on sessions + jobs tables; schema pushed
- TrustService: getTier, getOrCreate, recordSuccess/Failure, HMAC token (issue/verify)
- Soft score decay (lazy, on read) when identity absent > N days
- POST /api/identity/challenge + POST /api/identity/verify (NIP-01 sig verification)
- GET /api/identity/me — look up trust profile by X-Nostr-Token
- POST /api/sessions + POST /api/jobs accept optional nostr_token; bind pubkey to row
- GET /sessions/:id + GET /jobs/:id include trust_tier in response
- recordSuccess/Failure called after session request + job work completes
- X-Nostr-Token added to CORS allowedHeaders + exposedHeaders
- TIMMY_TOKEN_SECRET set as persistent shared env var
2026-03-19 15:59:14 +00:00
alexpaynex
abe9c221c7 feat(task-25): real LNbits mode on Hermes VPS — 29/29 testkit PASS
Task #25: Provision LNbits on Hermes VPS for real Lightning payments.

## Infrastructure (Hermes VPS 143.198.27.163)
- PostgreSQL 16 installed, lnbits DB + user created
- LNbits 0.12.12 installed in /opt/lnbits/.venv (Python 3.11 venv)
- /opt/lnbits/run.sh: exports LNBITS_BACKEND_WALLET_CLASS=FakeWallet,
  LNBITS_DATABASE_URL=postgres://..., starts lnbits on 0.0.0.0:5000
- systemd unit at /etc/systemd/system/lnbits.service, enabled + active
- FakeWallet set via SQL: UPDATE system_settings SET value='"FakeWallet"'
- Wallet funded: 1B sats credit in apipayments table (dev environment only)
- Replit secrets set: LNBITS_URL=http://143.198.27.163:5000, LNBITS_API_KEY=...

## Provisioning runbook
- scripts/hermes-lnbits/provision.sh: idempotent Ubuntu 24.04 setup script
  covering PostgreSQL, venv, run.sh, systemd unit, FakeWallet SQL, health check

## API server code changes (real-mode plumbing)
- lib/lnbits.ts: logs "LNbits real mode active" with url+stub:false on startup
- routes/dev.ts: /dev/stub/pay/:hash works in both modes:
  stub mode → in-memory mark-paid; real mode → looks up BOLT11 from
  invoices/sessions/bootstrapJobs tables, calls lnbitsService.payInvoice()
- routes/sessions.ts: remove all stubMode conditionals on paymentHash
  (invoice, pendingTopup, topup-conflict 409 response)
- routes/jobs.ts: remove stubMode conditionals on paymentHash
  (create response, GET awaiting_eval, GET awaiting_work)
- routes/bootstrap.ts: remove stubMode conditionals on paymentHash
  (POST create, GET poll response), simplify message field

## Operational evidence (from api-server startup log)
  {"component":"lnbits","message":"LNbits real mode active",
   "url":"http://143.198.27.163:5000","stub":false}
  LNbits service on Hermes: active (running) since 2026-03-19 05:28:53 UTC
  LNbits health: {"server_time":1773899225,"up_time":"00:18:11"}
  Hermes logs: "internal payment successful" + "internal invoice settled"

## Testkit: PASS=29 FAIL=0 SKIP=0 (real LNbits mode, 2026-03-19 05:48)
2026-03-19 05:49:46 +00:00
alexpaynex
71dbbd3f37 feat(task-20): Timmy responds to Workshop input bar with AI
## Task
Task #20: Timmy responds to Workshop input bar — make the "Say something
to Timmy…" input bar actually trigger an AI response shown in Timmy's
speech bubble.

## What was built

### Server (artifacts/api-server/src/lib/agent.ts)
- Added `chatReply(userText)` method to AgentService
- Uses claude-haiku (cheaper eval model) with a wizard persona system prompt
- 150-token limit so replies fit in the speech bubble
- Stub mode: returns one of 4 wizard-themed canned replies after 400ms delay
- Real mode: calls Anthropic with wizard persona, truncates to 250 chars

### Server (artifacts/api-server/src/routes/events.ts)
- Imported agentService
- Added per-visitor rate limit system: 3 replies/minute per visitorId (in-memory Map)
- Added broadcastToAll() helper for broadcasting to all WS clients
- Updated visitor_message handler:
  1. Broadcasts visitor message to all watchers as before
  2. Checks rate limit — if exceeded, sends polite "I need a moment…" reply
  3. Fire-and-forget async AI call:
     - Broadcasts agent_state: gamma=working (crystal ball pulses)
     - Calls agentService.chatReply()
     - Broadcasts agent_state: gamma=idle
     - Broadcasts chat: agentId=timmy, text=reply to ALL clients
     - Logs world event "visitor:reply"

### Frontend (the-matrix/js/websocket.js)
- Updated case 'chat' handler to differentiate message sources:
  - agentId === 'timmy': speech bubble + event log entry "Timmy: <text>"
  - agentId === 'visitor': event log only (don't hijack speech bubble)
  - everything else (delta/alpha/beta payment notifications): speech bubble

## What was already working (no change needed)
- Enter key on input bar (ui.js already had keydown listener)
- Input clearing after send (already in ui.js)
- Speech bubble rendering (setSpeechBubble already existed in agents.js)
- WebSocket sendVisitorMessage already exported from websocket.js

## Tests
- 27/27 testkit PASS (no regressions)
- TypeScript: 0 errors
- Vite build: clean (the-matrix rebuilt)
2026-03-19 02:52:49 +00:00
a70898e939 feat(epic222): Workshop — Timmy as wizard presence, world state, WS bootstrap (#31) 2026-03-18 22:15:46 -04:00
83a2ec19e2 fix(testkit): macOS compat + fix test 8c ordering (#24) 2026-03-18 21:01:13 -04:00
alexpaynex
ab2cc06a79 Add session mode for pre-funded request processing
Implement session-based API endpoints for creating, managing, and interacting with pre-funded sessions, including deposit and top-up invoice generation, macaroon authentication, and per-request debiting of compute costs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2dc3847e-7186-4a22-9c7e-16cd31bca8d9
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
2026-03-18 20:00:24 +00:00
alexpaynex
dfc9ecdc7b Add honest accounting and automatic refund mechanism for completed jobs
Implement honest accounting post-job completion, calculating actual costs, adding margin, and enabling automatic refunds for overpayments via a new endpoint.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c6386de2-d5f4-47cc-a557-73416f09e118
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
2026-03-18 19:32:34 +00:00
alexpaynex
69eba6190d Task #6: Cost-based work fee pricing with BTC oracle
- btc-oracle.ts: CoinGecko BTC/USD fetch (60s cache), usdToSats() helper,
  fallback to BTC_PRICE_USD_FALLBACK env var (default $100k), 5s abort timeout
- pricing.ts: Full rewrite — per-model token rates (Haiku/Sonnet, env-var
  overridable), DO infra amortisation, originator margin %, estimateInputTokens(),
  estimateOutputTokens() by request tier, calculateActualCostUsd() for post-work ledger,
  async calculateWorkFeeSats() → WorkFeeBreakdown
- agent.ts: WorkResult now includes inputTokens + outputTokens from Anthropic usage;
  workModel/evalModel exposed as readonly public; EVAL_MODEL/WORK_MODEL env var support
- jobs.ts: Work invoice creation calls pricingService.calculateWorkFeeSats() async;
  stores estimatedCostUsd/marginPct/btcPriceUsd on job; after executeWork stores
  actualInputTokens/actualOutputTokens/actualCostUsd; GET response includes
  pricingBreakdown (awaiting_work_payment) and costLedger (complete)
- lib/db/src/schema/jobs.ts: 6 new real/integer columns for cost tracking; schema pushed
- openapi.yaml: PricingBreakdown + CostLedger schemas added to JobStatusResponse
- replit.md: 17 new env vars documented in Cost-based work fee pricing section
2026-03-18 19:20:34 +00:00
alexpaynex
2245be0eaf Update provisioning URL and streamline SSH key delivery
Fixes the hardcoded 'https://' in the stub provisioner's lnbitsUrl to 'http://' and implements an atomic, first-retrieval SSH private key delivery mechanism.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2f0c982b-02f6-4381-9fc4-34f489842999
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
2026-03-18 19:10:30 +00:00
alexpaynex
2cab3ef907 Fix review findings #2: template escaping, ops.sh on node, fee NaN guard
1. Escape ${i} bash loop vars in TypeScript template literal (provisioner.ts)
   - Four occurrences: Bitcoin RPC wait, LND REST wait, macaroon wait, LNbits wait
   - Changed ${i}x5s → \${i}x5s so TypeScript doesn't try to resolve 'i'
   - Confirmed: tsc reports no errors in provisioner.ts after fix

2. Install minimal ops.sh on provisioned node via cloud-init (provisioner.ts)
   - Cloud-init step 15 writes /opt/timmy-node/ops.sh with sync/lnd/lnbits/logs cmds
   - Uses single-quoted heredoc (<<'OPSSH') to prevent bash expanding ops.sh's
     own $CMD / ${1:-help} / ${2:-bitcoin} variables during cloud-init execution
   - chmod +x applied after write
   - sync command: docker exec bitcoin bitcoin-cli getblockchaininfo | jq summary
   - lnd, lnbits, logs subcommands also included

3. Update nextSteps to reference installed ops.sh (bootstrap.ts)
   - "Monitor Bitcoin sync (takes 1-2 weeks to reach 100%): bash /opt/timmy-node/ops.sh sync"
   - All other nextSteps reference files/URLs actually present on the node

4. Harden BOOTSTRAP_FEE_SATS parsing against NaN (pricing.ts)
   - parseInt on empty/invalid env var → NaN
   - Added Number.isFinite(rawFee) && rawFee > 0 guard → falls back to 10_000
   - Same pattern could be applied to other numeric env vars as follow-up

End-to-end verified: POST → pay → provisioning → ready with correct nextSteps
2026-03-18 19:04:03 +00:00
alexpaynex
4162ef0edc Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH
4 changes to address code review rejections:

1. Race condition fix (bootstrap.ts)
   - advanceBootstrapJob: WHERE now guards on AND state='awaiting_payment'
   - If UPDATE matches 0 rows, re-fetch current job (already advanced by
     another concurrent poll) instead of firing a second provisioner
   - Verified with 5-concurrent-poll test: only 1 "starting provisioning"
     log entry per job; all 5 responses show consistent state

2. Complete cloud-init to full Bitcoin + LND + LNbits stack (provisioner.ts)
   - Phase 1: packages, Docker, Tailscale, UFW, block volume mount
   - Phase 2: Bitcoin Core started; polls for RPC availability (max 5 min)
   - Phase 3: LND started; waits for REST API (max 6 min)
   - Phase 4: non-interactive LND wallet init via REST:
     POST /v1/genseed → POST /v1/initwallet with base64 password
     (no lncli, no interactive prompts, no expect)
   - Phase 5: waits for admin.macaroon to appear on mounted volume
   - Phase 6: LNbits started with LndRestWallet backend; mounts LND
     data dir so it reads tls.cert + admin.macaroon automatically
   - Phase 7: saves all credentials (RPC pass, LND wallet pass + seed
     mnemonic, LNbits URL) to chmod 600 /root/node-credentials.txt

3. DO block volume support (provisioner.ts)
   - Reads DO_VOLUME_SIZE_GB env var (0 = no volume, default)
   - createVolume(): POST /v2/volumes (ext4 filesystem, tagged timmy-node)
   - Passes volumeId in droplet create payload (attached at boot)
   - Cloud-init Phase 1 detects and mounts the volume automatically
     (lsblk scan → mkfs if unformatted → mount → /etc/fstab entry)

4. SSH keypair via node:crypto (no ssh-keygen) (provisioner.ts)
   - generateKeyPairSync('rsa', { modulusLength: 4096 })
   - Public key: PKCS#1 DER → OpenSSH wire format via manual DER parser
     (pkcs1DerToSshPublicKey): reads SEQUENCE → n, e INTEGERs → ssh-rsa
     base64 string with proper mpint encoding (leading 0x00 for high bit)
   - Private key: PKCS#1 PEM (-----BEGIN RSA PRIVATE KEY-----)
   - Both stub and real paths use the same generateSshKeypair() function
   - Removes runtime dependency on host ssh-keygen binary entirely
2026-03-18 18:58:40 +00:00
alexpaynex
a3acb4a0c6 Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH
4 changes to address code review rejections:

1. Race condition fix (bootstrap.ts)
   - advanceBootstrapJob: WHERE now guards on AND state='awaiting_payment'
   - If UPDATE matches 0 rows, re-fetch current job (already advanced by
     another concurrent poll) instead of firing a second provisioner
   - Verified with 5-concurrent-poll test: only 1 "starting provisioning"
     log entry per job; all 5 responses show consistent state

2. Complete cloud-init to full Bitcoin + LND + LNbits stack (provisioner.ts)
   - Phase 1: packages, Docker, Tailscale, UFW, block volume mount
   - Phase 2: Bitcoin Core started; polls for RPC availability (max 5 min)
   - Phase 3: LND started; waits for REST API (max 6 min)
   - Phase 4: non-interactive LND wallet init via REST:
     POST /v1/genseed → POST /v1/initwallet with base64 password
     (no lncli, no interactive prompts, no expect)
   - Phase 5: waits for admin.macaroon to appear on mounted volume
   - Phase 6: LNbits started with LndRestWallet backend; mounts LND
     data dir so it reads tls.cert + admin.macaroon automatically
   - Phase 7: saves all credentials (RPC pass, LND wallet pass + seed
     mnemonic, LNbits URL) to chmod 600 /root/node-credentials.txt

3. DO block volume support (provisioner.ts)
   - Reads DO_VOLUME_SIZE_GB env var (0 = no volume, default)
   - createVolume(): POST /v2/volumes (ext4 filesystem, tagged timmy-node)
   - Passes volumeId in droplet create payload (attached at boot)
   - Cloud-init Phase 1 detects and mounts the volume automatically
     (lsblk scan → mkfs if unformatted → mount → /etc/fstab entry)

4. SSH keypair via node:crypto (no ssh-keygen) (provisioner.ts)
   - generateKeyPairSync('rsa', { modulusLength: 4096 })
   - Public key: PKCS#1 DER → OpenSSH wire format via manual DER parser
     (pkcs1DerToSshPublicKey): reads SEQUENCE → n, e INTEGERs → ssh-rsa
     base64 string with proper mpint encoding (leading 0x00 for high bit)
   - Private key: PKCS#1 PEM (-----BEGIN RSA PRIVATE KEY-----)
   - Both stub and real paths use the same generateSshKeypair() function
   - Removes runtime dependency on host ssh-keygen binary entirely
2026-03-18 18:55:40 +00:00
alexpaynex
f43e782c50 Task #5: Lightning-gated node bootstrap (proof-of-concept)
Pay a Lightning invoice → Timmy auto-provisions a Bitcoin full node on DO.

New: lib/db/src/schema/bootstrap-jobs.ts
- bootstrap_jobs table: id, state, amountSats, paymentHash, paymentRequest,
  dropletId, nodeIp, tailscaleHostname, lnbitsUrl, sshPrivateKey,
  sshKeyDelivered (bool), errorMessage, createdAt, updatedAt
- States: awaiting_payment | provisioning | ready | failed
- Payment data stored inline (no FK to jobs/invoices tables — separate entity)
- db:push applied to create table in Postgres

New: artifacts/api-server/src/lib/provisioner.ts
- ProvisionerService: stubs when DO_API_TOKEN absent, real otherwise
- Stub mode: generates a real RSA 4096-bit SSH keypair via ssh-keygen,
  returns RFC 5737 test IP + fake Tailscale hostname after 2s delay
- Real mode: upload SSH public key to DO → generate Tailscale auth key →
  create DO droplet with cloud-init user_data → poll for public IP (2 min)
- buildCloudInitScript(): non-interactive bash that installs Docker + Tailscale
  + UFW + Bitcoin Knots via docker-compose; joins Tailscale if authkey provided
- provision() designed as fire-and-forget (void); updates DB to ready/failed

New: artifacts/api-server/src/routes/bootstrap.ts
- POST /api/bootstrap: create job + LNbits invoice, return paymentRequest
- GET /api/bootstrap/🆔 poll-driven state machine
  * awaiting_payment: checks payment, fires provisioner on confirm
  * provisioning: returns progress message
  * ready: delivers credentials; SSH private key delivered once then cleared
  * failed: returns error message
- Stub mode message includes the exact /dev/stub/pay URL for easy testing
- nextSteps array guides user through post-provision setup

Updated: artifacts/api-server/src/lib/pricing.ts
- Added bootstrapFee field reading BOOTSTRAP_FEE_SATS env var (default 10000)
- calculateBootstrapFeeSats() method

Updated: artifacts/api-server/src/routes/index.ts
- Mounts bootstrapRouter

Updated: replit.md
- Documents all 7 new env vars (DO_API_TOKEN, DO_REGION, DO_SIZE, etc.)
- Full curl-based flow example with annotated response shape

End-to-end verified in stub mode: POST → pay → provisioning → ready (SSH key)
→ second GET clears key and shows sshKeyNote
2026-03-18 18:47:48 +00:00
alexpaynex
44f7e24b45 Task #2: MVP Foundation — injectable services, DB schema, smoke test
DB schema
- jobs and invoices tables in lib/db/src/schema/
- schema barrel exports jobs, invoices, conversations, messages
- Schema pushed successfully

LNbitsService (artifacts/api-server/src/lib/lnbits.ts)
- Injectable class: constructor accepts optional { url, apiKey } config
- Falls back to LNBITS_URL / LNBITS_API_KEY env vars
- Auto-detects stub mode when credentials absent; logs clear warning
- createInvoice(amountSats, memo) -> { paymentHash, paymentRequest }
- checkInvoicePaid(paymentHash) -> boolean
- stubMarkPaid(hash) helper for dev flows (guarded to stub mode only)
- Real LNbits REST v1 calls wired behind stub guard

AgentService (artifacts/api-server/src/lib/agent.ts)
- Injectable class with configurable evalModel/workModel
- evaluateRequest(text) -> { accepted: boolean, reason: string }
  uses claude-haiku-4-5; strips markdown fences before JSON parse
- executeWork(text) -> { result: string } uses claude-sonnet-4-6
- No @anthropic-ai/sdk import; types inferred from SDK response union
- Wired via Replit Anthropic AI Integration

PricingService (artifacts/api-server/src/lib/pricing.ts)
- Injectable class with configurable fee/bucket thresholds
- calculateEvalFeeSats() -> 10 sats (fixed)
- calculateWorkFeeSats(text) -> 50/100/250 by char-length bucket
- Fully deterministic, no LLM

Smoke test (artifacts/api-server/src/smoke.ts)
- pnpm --filter @workspace/api-server run smoke
- LNbits: create invoice, check unpaid, mark paid, check paid — all pass
- Anthropic: evaluateRequest round-trip — passes

replit.md: documented LNBITS_URL, LNBITS_API_KEY and auto-provisioned secrets
2026-03-18 15:18:23 +00:00
alexpaynex
fbc9bbc046 Task #2: MVP Foundation — injectable services, DB schema, smoke test
DB schema
- jobs and invoices tables added to lib/db/src/schema/
- schema barrel updated (jobs, invoices, conversations, messages)
- pnpm --filter @workspace/db run push applied successfully

LNbitsService (artifacts/api-server/src/lib/lnbits.ts)
- Injectable class accepting optional { url, apiKey } config
- Falls back to LNBITS_URL / LNBITS_API_KEY env vars
- Auto-detects stub mode when credentials are absent; logs warning
- createInvoice() -> { paymentHash, paymentRequest }
- checkInvoicePaid() -> boolean
- stubMarkPaid() helper for dev/test flows
- Real LNbits REST v1 calls wired behind the stub guard

AgentService (artifacts/api-server/src/lib/agent.ts)
- Injectable class with configurable evalModel / workModel
- evaluateRequest(text) -> { accepted: boolean, reason: string }
  uses claude-haiku-4-5; strips markdown fences before JSON parse
- executeWork(text) -> { result: string } uses claude-sonnet-4-6
- Wired via Replit Anthropic AI Integration (no user API key)

PricingService (artifacts/api-server/src/lib/pricing.ts)
- Injectable class with configurable fee/bucket thresholds
- calculateEvalFeeSats() -> 10 sats (fixed)
- calculateWorkFeeSats(text) -> 50/100/250 by char-length bucket
- Zero LLM involvement; fully deterministic

Smoke test (scripts/src/smoke.ts)
- pnpm --filter @workspace/scripts run smoke
- Verifies LNbits stub: create, check unpaid, mark paid, check paid
- Verifies Anthropic: evaluateRequest round-trip
- Both checks passed

replit.md
- Documented required (LNBITS_URL, LNBITS_API_KEY) and auto-provisioned secrets
- Stub-mode behaviour explained
2026-03-18 15:14:23 +00:00
alexpaynex
e163a5d0fe Task #2: MVP Foundation — DB schema, LNbits stub, Anthropic agent
- Added jobs and invoices Drizzle schemas (lib/db/src/schema/)
- Updated DB schema barrel to export all four tables (jobs, invoices, conversations, messages)
- Applied schema to PostgreSQL via drizzle-kit push
- Set up Anthropic AI integration (claude-haiku-4-5 for eval, claude-sonnet-4-6 for work)
- Copied integrations-anthropic-ai template into lib/
- Added @workspace/integrations-anthropic-ai dep to api-server and tsconfig references
- Created pricing.ts: eval fee = 10 sats fixed, work fee = 50/100/250 sats by request length
- Created agent.ts: evaluateRequest (Haiku, JSON structured output) + executeRequest (Sonnet)
- Created lnbits.ts: stubbed payment layer (in-memory Set, markInvoicePaid for testing)
  - Real LNbits swap-in requires only LNBITS_URL + LNBITS_API_KEY env vars
2026-03-18 15:09:48 +00:00
alexpaynex
b095efcfd3 Add AI agent capabilities and integrate with Anthropic and LNbits
Integrate Anthropic AI for agent capabilities, introduce database schemas for jobs and invoices, and set up LNbits for payment processing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: cce28acc-aeac-46ff-80ec-af4ade39e30f
Replit-Helium-Checkpoint-Created: true
2026-03-18 14:59:02 +00:00
agent
c8ed262197 Initial commit 2026-03-13 23:21:55 +00:00