Files
timmy-tower/the-matrix/js/timmy-id.js
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

63 lines
1.6 KiB
JavaScript

/**
* timmy-id.js — Timmy's identity card.
*
* Fetches Timmy's Nostr npub from GET /api/identity/timmy and populates
* the #timmy-id-card widget. Npub is displayed shortened and is copyable.
* Zap count refreshes every 60 seconds.
*/
const REFRESH_INTERVAL_MS = 60_000;
function shortenNpub(npub) {
if (!npub || npub.length < 16) return npub;
return npub.slice(0, 10) + '…' + npub.slice(-6);
}
async function fetchTimmyIdentity() {
try {
const res = await fetch('/api/identity/timmy');
if (!res.ok) return;
return await res.json();
} catch {
return null;
}
}
function renderCard(data) {
if (!data) return;
const $npub = document.getElementById('timmy-npub');
const $zapCount = document.getElementById('timmy-zap-count');
if ($npub && data.npub) {
$npub.textContent = shortenNpub(data.npub);
$npub.title = data.npub + '\n(click to copy)';
if (!$npub._clickBound) {
$npub._clickBound = true;
$npub.addEventListener('click', () => {
navigator.clipboard.writeText(data.npub).then(() => {
const orig = $npub.textContent;
$npub.textContent = 'copied!';
setTimeout(() => { $npub.textContent = orig; }, 1500);
}).catch(() => {});
});
}
}
if ($zapCount) {
const n = typeof data.zapCount === 'number' ? data.zapCount : 0;
$zapCount.textContent = `${n} zap${n === 1 ? '' : 's'} sent`;
}
}
export async function initTimmyId() {
const data = await fetchTimmyIdentity();
renderCard(data);
setInterval(async () => {
const refreshed = await fetchTimmyIdentity();
renderCard(refreshed);
}, REFRESH_INTERVAL_MS);
}