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
This commit is contained in:
Replit Agent
2026-03-19 19:27:13 +00:00
parent dabadb4298
commit eb5dcfd48a
12 changed files with 649 additions and 1 deletions

View File

@@ -8,3 +8,5 @@ export * from "./sessions";
export * from "./nostr-identities";
export * from "./timmy-config";
export * from "./free-tier-grants";
export * from "./timmy-nostr-events";
export * from "./nostr-trust-vouches";

View File

@@ -0,0 +1,20 @@
import { pgTable, text, integer, timestamp } from "drizzle-orm/pg-core";
import { nostrIdentities } from "./nostr-identities";
export const nostrTrustVouches = pgTable("nostr_trust_vouches", {
id: text("id").primaryKey(),
voucherPubkey: text("voucher_pubkey")
.notNull()
.references(() => nostrIdentities.pubkey),
voucheePubkey: text("vouchee_pubkey").notNull(),
vouchEventJson: text("vouch_event_json").notNull(),
trustBoost: integer("trust_boost").notNull().default(20),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type NostrTrustVouch = typeof nostrTrustVouches.$inferSelect;

View File

@@ -0,0 +1,19 @@
import { pgTable, text, integer, timestamp } from "drizzle-orm/pg-core";
export const timmyNostrEvents = pgTable("timmy_nostr_events", {
id: text("id").primaryKey(),
kind: integer("kind").notNull(),
recipientPubkey: text("recipient_pubkey"),
eventJson: text("event_json").notNull(),
amountSats: integer("amount_sats"),
relayUrl: text("relay_url"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type TimmyNostrEvent = typeof timmyNostrEvents.$inferSelect;