## Code review round 2 issues resolved
### Vouch replay / duplicate boost vulnerability — FIXED
- `nostr-trust-vouches.ts` schema: added `eventId` column + two unique guards:
1. `UNIQUE(event_id)` — same signed event cannot be replayed for any pair
2. `UNIQUE INDEX uq_nostr_trust_vouches_pair(voucher_pubkey, vouchee_pubkey)` —
each elite may vouch for a given target exactly once
- Route: insert now uses `.onConflictDoNothing().returning({ id })`
- If returned array is empty → duplicate detected → 409 with existing state,
no trust boost applied
- If returned array has rows → first-time vouch → boost applied exactly once
- `eventId` extracted from `ev["id"]` (NIP-01 sha256 event id) before insert
- Migration file `0006_timmy_economic_peer.sql` updated to include both
unique constraints (UNIQUE + CREATE UNIQUE INDEX)
- Schema pushed to production — all three indexes confirmed in DB:
`nostr_trust_vouches_event_id_unique`, `uq_nostr_trust_vouches_pair`, `pkey`
### Previously fixed (round 1)
- LNURL-pay resolution in ZapService (full NIP-57 §4 flow)
- Vouch event made required with p-tag vouchee binding
- DB migration file 0006 created for both new tables + lightning_address column
- GET /identity/timmy now returns relayUrl field
### Verified
- TypeScript: 0 errors (tsc --noEmit clean)
- DB: all constraints confirmed live in production
- API: /identity/timmy 200, /identity/challenge nonce, /identity/vouch 401/400
## 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
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
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 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
- 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 from all recordGrant() call sites
- All three call sites now use await; failures propagate correctly
Fix 7 — Add migration 0005_free_tier.sql
- Creates timmy_config, free_tier_grants tables
- Adds nostr_identities.sats_absorbed_today / absorbed_reset_at columns
- Adds jobs.free_tier / absorbed_sats columns
- Adds sessions.nostr_pubkey FK column (for migration-driven deploys)
- All IF NOT EXISTS — safe to run on already-pushed DBs
## 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)
- sessions.ts / jobs.ts schema: add .references(() => nostrIdentities.pubkey) FK constraints
on nostrPubkey columns; import without .js extension for drizzle-kit CJS compat
- Schema pushed to DB (FK constraints now enforced at DB level)
- sessions route: call getOrCreate before insert to guarantee FK target exists;
recordFailure now covers both 'rejected' AND 'failed' final states
- jobs route: call getOrCreate before insert; recordFailure added in
runEvalInBackground for rejected and failed states; recordFailure added in
runWorkInBackground catch block for failed state
- All GET/POST endpoints now always return trust_tier (anonymous fallback)
- Full typecheck clean; schema pushed; smoke tested — all routes green
- 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
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
## 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
- 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
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
Adds a foreign key constraint to the `invoices` table, referencing the `id` column in the `jobs` table, ensuring data integrity.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d9dd1cc2-bc61-41c5-90bb-94504f4308a9
Replit-Helium-Checkpoint-Created: true
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