Files
timmy-tower/artifacts/api-server/src/lib/timmy-identity.ts
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

78 lines
2.6 KiB
TypeScript

/**
* timmy-identity.ts — Timmy's persistent Nostr identity.
*
* Loads a secp256k1 keypair from TIMMY_NOSTR_NSEC env var at boot.
* If the env var is absent, generates an ephemeral keypair and logs
* a warning instructing the operator to persist it.
*
* Exports `timmyIdentityService` singleton with:
* .npub — bech32 public key (npub1...)
* .nsec — bech32 private key (nsec1...) — never log this
* .pubkeyHex — raw 64-char hex pubkey
* .sign(template) — finalises and signs a Nostr event template
* .encryptDm(recipientPubkeyHex, plaintext) — NIP-04 encrypt
*/
import { generateSecretKey, getPublicKey, finalizeEvent, nip19, nip04 } from "nostr-tools";
import type { EventTemplate, NostrEvent } from "nostr-tools";
import { makeLogger } from "./logger.js";
const logger = makeLogger("timmy-identity");
function loadOrGenerate(): { sk: Uint8Array; nsec: string; npub: string; pubkeyHex: string } {
const raw = process.env["TIMMY_NOSTR_NSEC"];
if (raw && raw.startsWith("nsec1")) {
try {
const decoded = nip19.decode(raw);
if (decoded.type !== "nsec") throw new Error("not nsec type");
const sk = decoded.data as Uint8Array;
const pubkeyHex = getPublicKey(sk);
const npub = nip19.npubEncode(pubkeyHex);
logger.info("timmy identity loaded from env", { npub });
return { sk, nsec: raw, npub, pubkeyHex };
} catch (err) {
logger.warn("TIMMY_NOSTR_NSEC is set but could not be decoded — generating ephemeral key", {
error: err instanceof Error ? err.message : String(err),
});
}
}
const sk = generateSecretKey();
const pubkeyHex = getPublicKey(sk);
const nsec = nip19.nsecEncode(sk);
const npub = nip19.npubEncode(pubkeyHex);
logger.warn(
"TIMMY_NOSTR_NSEC not set — generated ephemeral Nostr identity. " +
"Set this env var permanently to preserve Timmy's identity across restarts.",
{ npub },
);
return { sk, nsec, npub, pubkeyHex };
}
class TimmyIdentityService {
readonly npub: string;
readonly nsec: string;
readonly pubkeyHex: string;
private readonly _sk: Uint8Array;
constructor() {
const identity = loadOrGenerate();
this.npub = identity.npub;
this.nsec = identity.nsec;
this.pubkeyHex = identity.pubkeyHex;
this._sk = identity.sk;
}
sign(template: EventTemplate): NostrEvent {
return finalizeEvent(template, this._sk);
}
async encryptDm(recipientPubkeyHex: string, plaintext: string): Promise<string> {
return nip04.encrypt(this._sk, recipientPubkeyHex, plaintext);
}
}
export const timmyIdentityService = new TimmyIdentityService();