Commit Graph

67 Commits

Author SHA1 Message Date
alexpaynex
35babd2400 Task #22: Slap/ragdoll physics on Timmy
## What was done
- agents.js: spring physics (stiffness=7, damping=0.8, clamp ±0.44 rad) on
  timmy.group.rotation.x/z via slapOffset/slapVelocity integrated per-frame with
  proper dt (capped at 50ms for tab-background safety)
- agents.js: applySlap(hitPoint) — computes XZ impulse direction from hit point
  relative to TIMMY_POS, adds angular velocity, triggers pip startle + crystal flash
- agents.js: _playBoing() — lazy AudioContext, sine oscillator 260→90 Hz with
  exponential gain decay (0.38s)
- agents.js: Pip startle — 3s decay timer, random scatter direction offset, 4x
  spin speed while startled, boosted pip light intensity
- agents.js: Crystal ball hit flash — hitFlashTimer=0.5s, intensity spikes to 10
  and fades; normal crystalLight/cbMat logic when not flashing
- agents.js: getTimmyGroup() export for raycaster target
- interaction.js: registerSlapTarget(group, applyFn) — stores targets
- interaction.js: _onPointerDown capture-phase listener — raycasts against
  timmyGroup recursively, calls applySlap on hit, suppresses OrbitControls drag
  for 220ms via stopImmediatePropagation + controls.enabled toggle
- main.js: imports getTimmyGroup, applySlap, registerSlapTarget; wires
  registerSlapTarget(getTimmyGroup(), applySlap) after initInteraction

## Verification
- Vite build: clean, 14 modules, 0 errors
- /tower HTTP 200
- Testkit: 27/27 PASS
2026-03-19 03:31:01 +00:00
alexpaynex
2956cc07b2 Update character's appearance to include a long grey wizard beard
Modify the `agents.js` file to replace the existing beard and moustache geometry with a new, longer grey beard model.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1df11cd3-4638-4860-9c27-dd3c2040f00f
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 03:24:31 +00:00
alexpaynex
93bd48f8ea Update Timmy's appearance to match reference with new colors and details
Refactors the `buildTimmy` function to update Timmy's robe color to royal purple, add celestial gold star decorations, and implement a silver beard and hair, along with a pulsing orange magic orb effect.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7cc95df8-ef94-4761-8b47-9c13fedbba9a
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 03:23:28 +00:00
alexpaynex
6e982ff772 Improve mouth geometry performance by precomputing all shapes
Optimize mouth geometry generation by precomputing a cache of shapes, eliminating runtime allocations and reducing garbage collection pressure during animations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 92d6d2fb-91ca-4dea-98b6-ff5053cb5ac7
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 03:18:20 +00:00
alexpaynex
8d48eb06b3 feat(task-21): Timmy face expressions + emotion engine
## What changed
- the-matrix/js/agents.js — complete face expression system on Timmy wizard

## Face geometry (all parented to head — follow head.rotation.z tilt)
- White sclera eyes (MeshStandardMaterial f5f2e8, emissive 0x777777@0.10)
  replace old flat dark-blue eye spheres
- Dark pupil spheres (MeshBasicMaterial 0x07070f) as children of each sclera;
  they scale with the parent eye for squint + animate independently for dilation
- Mouth arc: TubeGeometry via QuadraticBezierCurve3; ctrlY = -smileAmount*0.065;
  rebuilt only when |smileDelta| > 0.016 (throttled, not per-frame GC)
- All face meshes are children of `head` mesh — head.rotation.z carries every
  face feature naturally with the existing head-tilt animation

## FACE_TARGETS table (lidScale, pupilScale, smileAmount)
- idle  (contemplative): 0.44 / 0.90 /  0.08 — half-lid, neutral
- active (curious):      0.92 / 1.25 /  0.38 — wide open + dilated pupils, smile
- thinking (focused):    0.30 / 0.72 /  0.00 — narrow squint + constrict, flat mouth
- working (attentive):   0.75 / 1.05 /  0.18 — alert/open eyes, slight grin

## setFaceEmotion(mood) — authoritative public API
- Accepts task-spec names (contemplative|curious|focused|attentive) and internal
  names (idle|active|thinking|working) via MOOD_ALIASES
- Sets timmy._overrideMood; persists across frames, takes precedence over
  deriveTimmyState() in updateAgents()
- Call with null to clear override and return to automatic state-driven expressions

## Per-frame lerp (rate 0.055/frame) in updateAgents
- Uses _overrideMood ?? deriveTimmyState() as effective mood
- lidScale → eyeL.scale.y / eyeR.scale.y (squash for squint/wide-open)
- pupilScale → pupilL/R.scale.setScalar() (uniform dilation)
- smileAmount → drives thresholded TubeGeometry rebuild

## Lip-sync while speaking (1 Hz, range 0.20–0.60)
- speechTimer > 0: smileTarget = 0.40 + sin(t*6.283)*0.20
- Returns to mood expression when timer expires

## Validation
- Vite build: clean (14 modules, no errors)
- testkit: 27/27 PASS (server restarted to clear rate-limit counters between runs)
2026-03-19 03:13:51 +00:00
alexpaynex
9ff5ef683d feat(task-21): Timmy face expressions + emotion engine
## What changed
- the-matrix/js/agents.js — face expression system added to Timmy wizard

## Face geometry (all parented to head — follow head.rotation.z tilt)
- White sclera eyes (MeshStandardMaterial f5f2e8, emissive 0x777777@0.10)
  replace the old flat dark-blue spheres
- Dark pupils (MeshBasicMaterial 0x07070f) as child meshes of each sclera;
  they scale with the parent eye for squint effect
- Mouth arc: TubeGeometry built from QuadraticBezierCurve3; control point
  moves ±0.065 on Y for smile/frown; rebuilt via _buildMouthGeo() only when
  |smileDelta| > 0.016 (throttled to avoid per-frame GC pressure)
- All face meshes are children of `head` — head.rotation.z carries every
  face component naturally with the existing head-tilt animation

## FACE_TARGETS lookup table (lidScale, pupilScale, smileAmount)
- idle  (contemplative): 0.44 / 0.90 / 0.08  — half-lid, neutral
- active (curious):      0.92 / 1.25 / 0.38  — wide eyes + dilated pupils, smile
- thinking (focused):    0.30 / 0.72 / -0.06 — squint + constricted pupils, flat
- working (attentive):   0.22 / 0.80 / 0.18  — very squint, slight grin

## setFaceEmotion(mood) exported API
- Accepts both task-spec names (contemplative|curious|focused|attentive)
  and internal state names (idle|active|thinking|working) via MOOD_ALIASES
- Immediately sets faceTarget; lerp in updateAgents() handles the smooth transition

## Per-frame lerp (rate 0.055/frame) in updateAgents
- lidScale → eyeL.scale.y / eyeR.scale.y (squash for squint)
- pupilScale → pupilL.scale / pupilR.scale (uniform dilation)
- smileAmount → drives TubeGeometry rebuild when drift > 0.016

## Lip-sync while speaking (~1 Hz)
- speechTimer > 0: smileTarget = 0.28 + sin(t*6.283)*0.22
- Returns to mood target when timer expires

## Validation
- Vite build: clean (14 modules, 542 kB, no errors)
- testkit: 27/27 PASS (after server restart to clear rate-limit counters)
2026-03-19 03:09:45 +00:00
alexpaynex
7f402c5c7f feat(task-21): Timmy face expressions + emotion engine
## What changed
- the-matrix/js/agents.js fully rewritten with face expression system

## Face geometry
- Replaced flat dark-blue eye spheres with white sclera (MeshStandardMaterial,
  emissive 0x777777@0.10, roughness 0.55) + dark pupils (MeshBasicMaterial 0x07070f)
  as child meshes of sclera
- Eyes are now children of the head mesh (not the group) so they naturally
  follow head.rotation.z tilts driven by the existing animation loop
- Mouth added as a canvas Sprite (128x32, always faces camera) parented to the
  group so it bobs with Timmy's body; drawn via quadraticCurveTo bezier arc

## Emotion → face parameter mapping (FACE_TARGETS table)
- idle (contemplative): lidScale=0.44, smileAmount=0.08  — half-lid, neutral
- active (curious):     lidScale=0.92, smileAmount=0.38  — wide eyes, smile
- thinking (focused):   lidScale=0.30, smileAmount=-0.06 — squint, flat mouth
- working (attentive):  lidScale=0.22, smileAmount=0.18  — very squint, slight grin

## Per-frame lerp (updateAgents)
- faceParams lerped toward faceTarget at rate 0.055/frame (smooth, no snap)
- eyeL.scale.y / eyeR.scale.y driven by faceParams.lidScale (squash = squint)
- Mouth canvas redrawn only when |smileDelta| > 0.016 or speakingChanged
  (avoids unnecessary texture uploads every frame)

## Lip-sync while speaking
- While speechTimer > 0: smileTarget = 0.28 + sin(t*6.283)*0.22 (~1 Hz)
- _drawMouth() renders two-lip "open mouth" shape when speaking=true
- Returns to mood expression when speechTimer expires

## Validation
- Vite build: clean (14 modules, 529 kB bundle, no errors)
- testkit: 27/27 PASS (no regressions)
- No out-of-scope changes (backend untouched)
2026-03-19 03:04:17 +00:00
alexpaynex
ad63b01223 Harden rate limit by using server-trusted IP address
Update rate limiting logic to use the server's IP address (extracted from request headers or socket) instead of the client-provided visitorId to prevent spoofing.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 892ae0fb-898b-4f34-949e-7a240560fe8e
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 02:56:36 +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
alexpaynex
4dd5937028 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 47814653-88b3-4dbb-8408-5a4d4164a83f
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 02:46:28 +00:00
4f7a5e9998 test: audit testkit — remove T3b inflation, add T17-T22 (27/27 PASS) (#32) 2026-03-18 22:34:12 -04:00
a70898e939 feat(epic222): Workshop — Timmy as wizard presence, world state, WS bootstrap (#31) 2026-03-18 22:15:46 -04:00
ea4cddc2ad fix(api): completedAt: null on non-complete states + OpenAPI timestamps & rate-limit headers (#29) 2026-03-18 21:49:51 -04:00
b929e6d72f feat(api): X-RateLimit-* headers on /api/demo + createdAt/completedAt on job responses (#19) (#28) 2026-03-18 21:41:14 -04:00
e088ca4cd8 feat(integration): WS bridge + Tower + payment panel + E2E test [10/10 PASS] (#26) 2026-03-18 21:20:51 -04:00
Replit Agent
3031c399ee docs: add Claude Opus 4.6 result to testkit results log (issue #25) 2026-03-19 01:04:50 +00:00
83a2ec19e2 fix(testkit): macOS compat + fix test 8c ordering (#24) 2026-03-18 21:01:13 -04:00
alexpaynex
ca94c0a9e5 Add Bitcoin/LND/LNbits local node setup scripts and node diagnostics endpoint
- scripts/bitcoin-ln-node/setup.sh: one-shot installer for Bitcoin Core (pruned mainnet), LND, and LNbits on Apple Silicon Mac. Generates secrets, writes configs, installs launchd plists for auto-start.
- scripts/bitcoin-ln-node/start.sh: start all services via launchctl; waits for RPC readiness and auto-unlocks LND wallet.
- scripts/bitcoin-ln-node/stop.sh: graceful shutdown (lncli stop → bitcoin-cli stop).
- scripts/bitcoin-ln-node/status.sh: full health check (Bitcoin sync %, LND channels/balance, LNbits HTTP, bore tunnel). Supports --json mode for machine consumption.
- scripts/bitcoin-ln-node/expose.sh: opens bore tunnel from LNbits port 5000 to bore.pub for Replit access.
- scripts/bitcoin-ln-node/get-lnbits-key.sh: fetches LNbits admin API key and prints Replit secret values.
- artifacts/api-server/src/routes/node-diagnostics.ts: GET /api/admin/node-status (JSON) and /api/admin/node-status/html — Timmy self-diagnoses its LNbits/LND connectivity and reports issues.
2026-03-18 21:58:41 +00:00
alexpaynex
4dd3c7f692 Show the application's public URL in server logs
Log the full public UI URL using the REPLIT_DEV_DOMAIN environment variable.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a00ebe7c-c8e0-4118-81aa-ae93770e942f
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-18 21:02:06 +00:00
alexpaynex
b02efc9057 Make job evaluation and execution run in the background
Refactors `runEvalInBackground` and `runWorkInBackground` to execute AI tasks asynchronously. Updates `pollJob` in `ui.ts` to handle 'evaluating', 'executing', and 'failed' states, and corrects `data.status` to `data.state` and `data.rejectionReason` to `data.reason`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ecf857ee-fa4d-47db-b4c1-b374ffb3815d
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-18 21:00:43 +00:00
alexpaynex
1b5c7045da Update screenshot showing application preview
Update attached image IMG_0028.jpeg to reflect the correct application preview.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 05656204-36d5-4ceb-9133-83b23ef131e5
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-18 20:44:59 +00:00
alexpaynex
e44d64ac44 Add payment hash to job creation response in stub mode
Include the `paymentHash` in the `evalInvoice` object when creating a job in stub mode via `POST /api/jobs` to ensure the frontend receives it correctly.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7c57683f-4afc-46df-83b9-8b259c160aea
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-18 20:36:49 +00:00
alexpaynex
feacdb7e45 Add screenshot of the application running on an iPhone
Add a screenshot image file to the attached assets directory.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 94b9ef9d-fe3f-474c-a584-f5987b095e5a
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-18 20:26:04 +00:00
alexpaynex
adde196a40 Task #7: Redirect root to Timmy UI
Added two redirect routes in artifacts/api-server/src/app.ts:
- GET / → 302 redirect to /api/ui
- GET /api → 302 redirect to /api/ui

This means opening the preview URL or the root of the app immediately
lands on the Timmy UI without any manual navigation.

No changes to the UI itself, no new routes, no new files.
Verified: both / and /api return HTTP 302 with Location: /api/ui.
2026-03-18 20:16:48 +00: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
e5bdae7159 Task #6: Cost-based work fee pricing with BTC oracle
## New files
- btc-oracle.ts: CoinGecko BTC/USD fetch (60s cache), usdToSats() helper (ceil, min 1 sat),
  5s abort timeout, fallback to BTC_PRICE_USD_FALLBACK env var (default $100k)
- lib/db/migrations/0002_cost_based_pricing.sql: SQL migration artifact adding 6 new columns
  to jobs table (estimated_cost_usd, margin_pct, btc_price_usd, actual_input_tokens,
  actual_output_tokens, actual_cost_usd); idempotent via ADD COLUMN IF NOT EXISTS

## Modified files
- pricing.ts: Full rewrite — per-model token rates (Haiku/Sonnet, env-var overridable),
  DO infra amortisation per request, originator margin %, estimateInputTokens/Output by 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
- lib/db/src/schema/jobs.ts: 6 new real/integer columns; schema pushed to DB
- jobs.ts route: Work invoice creation calls pricingService.calculateWorkFeeSats() async;
  stores estimatedCostUsd/marginPct/btcPriceUsd; post-work stores actualInputTokens/
  actualOutputTokens/actualCostUsd; GET response includes pricingBreakdown and costLedger
  with totalTokens (input + output computed field)
- openapi.yaml: PricingBreakdown + CostLedger schemas (with totalTokens) added
- lib/api-zod/src/generated/api.ts: Regenerated with new schemas
- lib/api-client-react/src/generated/api.schemas.ts: Regenerated (PricingBreakdown, CostLedger)
- replit.md: 17 new env vars documented in cost-based pricing section
2026-03-18 19:25:06 +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
bc78bfa452 Add Nostr integration to the roadmap for future development
Add a roadmap section to replit.md detailing the planned integration of Nostr for node credential delivery, job status events, and node identity, replacing current HTTP polling mechanisms.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 45e1ce2b-4846-4800-be09-ed16006cca5f
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:15:24 +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
1a60363b74 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b9677bee-c3c6-46af-805a-657389ff38b4
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 18:41:14 +00:00
alexpaynex
5dd80ee81a Add ability to sweep funds using xpub or a list of addresses
Implement multiple sweep destination modes (static, address list, xpub) with state management and update configuration scripts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8df121fd-c189-4c73-a76b-d9a3e07de783
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 18:38:16 +00:00
alexpaynex
e5f78e1eb9 Add interactive configuration for sweep thresholds and frequency
Introduce a new command `bash ops.sh configure-sweep` to interactively set and update auto-sweep parameters, including cold address, on-chain thresholds, and sweep frequency, while also updating the cron schedule and providing user-friendly feedback in `ops.sh sweep`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3c6bbb97-1029-4402-bba7-d04e3f992bd6
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 18:34:48 +00:00
alexpaynex
c45625fc98 Automate bitcoin sweeps to secure cold storage addresses
Fix cron job redirection to prevent log duplication and ensure transaction sending commands are handled correctly with error checking.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: eebfeaae-fd85-413b-a84e-99224a9b6b98
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 18:32:15 +00:00
alexpaynex
12db06cc26 Add auto-sweep hot wallet to cold storage (Task #4)
New file: infrastructure/sweep.sh
- Reads /opt/timmy-node/sweep.conf (COLD_ADDRESS, KEEP_SATS=300000, MIN_SWEEP=50000)
- Queries LND on-chain confirmed balance via lncli walletbalance
- Calculates sweep_amt = balance - KEEP_SATS; skips if < MIN_SWEEP
- Sends via lncli sendcoins --addr $COLD_ADDRESS --amt $sweep_amt
- Logs timestamp, balance, sweep amount, txid to /var/log/timmy-sweep.log
- Triggers ops.sh backup after every successful sweep
- Exits cleanly (no crash) if conf missing, address unset, or balance too low

Updated: infrastructure/setup.sh
- Copies sweep.sh and ops.sh to /opt/timmy-node/ during bootstrap
- Installs two cron jobs: sweep at 3am UTC, backup at 4am UTC
- Creates /var/log/timmy-sweep.log and /var/log/timmy-backup.log
- Idempotent: removes existing timmy-node cron entries before re-adding

Updated: infrastructure/lnd-init.sh
- New "Cold Storage Auto-Sweep Setup" section after LNbits wallet creation
- Prompts for cold Bitcoin address (optional — skip to configure later)
- Writes /opt/timmy-node/sweep.conf with address + documented defaults
- sweep.conf chmod 600 (sensitive — contains sweep destination)
- Graceful skip path with instructions for later configuration

Updated: infrastructure/ops.sh
- New `sweep` command: shows sweep.conf config, current on-chain balance,
  last 5 lines of /var/log/timmy-sweep.log
- New `run-sweep` command: triggers sweep.sh immediately outside cron
- Help text updated with both new commands
2026-03-18 18:30:28 +00:00
alexpaynex
bf759e5a42 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 49c71dd4-ad3f-4fe9-9031-e5874464e59b
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 18:28:21 +00:00
alexpaynex
8acc30d41e Update node to use Bitcoin Knots for improved flexibility
Switch to the Bitcoin Knots Docker image and adjust data directory paths and healthcheck commands in docker-compose.yml, lnd-init.sh, ops.sh, and setup.sh.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 66d0db34-90d1-4f85-b27d-f9f747253c18
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 18:18:08 +00:00
alexpaynex
88b5ebfa3c Set up Bitcoin node and Lightning infrastructure with Docker
Create Docker Compose configuration, Bitcoin and LND configuration files, and bootstrap/init scripts for setting up a Bitcoin full node, LND, and LNbits on a Digital Ocean droplet.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0b0f1422-94e9-40dc-9b10-29c5f33a1bac
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 18:13:29 +00:00
alexpaynex
0921fa1ca3 Make the demo user interface accessible through the API
Add a new UI route to serve the interactive demo interface at `/api/ui`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3fb69144-fc09-46cf-8560-9b4bc828c60f
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 18:06:44 +00:00
alexpaynex
ade318a917 Add documentation for alternative payment providers
Provide details on using Voltage, self-hosting LNbits, or integrating with Alby as alternatives to the main LNbits instance.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 35605e04-4842-4c88-b87c-8fb79cc0b0cb
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:58:54 +00:00
alexpaynex
001873c688 Update test plan and script for dual-mode payment system
Refactor TIMMY_TEST_PLAN.md and timmy_test.sh to support dual-mode payments (per-job and session-based). Add new tests for session endpoints and gracefully handle rate limiting in existing tests.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 290ed20c-1ddc-4b42-810d-8415dd3a9c08
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:53:21 +00:00
alexpaynex
fc4fd50e33 Add automated testing flow to reduce manual effort
Integrate a new testkit endpoint and update package.json scripts to enable automated testing via `pnpm test` and `pnpm test:prod`, including a new test case for request body size limits.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 60472e18-59b7-4877-a9a2-16381573ab68
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:43:01 +00:00
alexpaynex
f5811da508 Improve input validation and error messaging for user requests
Update API endpoints for jobs and demo routes to enforce a maximum character limit of 500 for the 'request' field. Refine error messages to distinguish between missing input and input exceeding the character limit.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 8c43b6a7-30d3-4806-8d46-3d364b17c284
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:33:21 +00:00
alexpaynex
53bc93a9b4 Add automated testing script and expose payment hashes
Integrates a new bash script for automated end-to-end testing of the Timmy API. Updates API routes to expose payment hashes in stub mode for easier invoice payment simulation during testing. Modifies test plan documentation to include the new automated script.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6f2776b0-a913-41d3-a988-759a82feb6f3
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:30:13 +00:00
alexpaynex
d24cc6fbe5 Add comprehensive test plan for evaluating the AI agent's API functionality
Add a new Markdown file containing a detailed test plan and report prompt for the AI agent API, and register it in the agent assets metadata.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: baaad612-0d55-41f8-983d-e1104c552e18
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:24:32 +00:00
alexpaynex
f785637945 Published your App
Replit-Commit-Author: Deployment
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: be88c3fd-cdde-409a-a807-c8bb934d7267
Replit-Commit-Deployment-Build-Id: 848bca0f-322f-442c-b1c2-4f19cfda133d
Replit-Helium-Checkpoint-Created: true
2026-03-18 17:19:38 +00:00