// Read-only scan: NIP-90 job requests (kinds 5000-5999) over the last 7 days on public relays. // Uses Node's built-in WebSocket. Sends only REQ/CLOSE (no EVENT publishing). const RELAYS = [ 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band', 'wss://relay.primal.net', 'wss://nostr.wine', 'wss://relay.snort.social', 'wss://offchain.pub', ]; const since = Math.floor(Date.now() / 1000) - 7 * 24 * 3600; const kinds = []; for (let k = 5000; k <= 5999; k++) kinds.push(k); const resultKinds = []; for (let k = 6000; k <= 6999; k++) resultKinds.push(k); const events = new Map(); const results = new Map(); const feedback = new Map(); function scan(url) { return new Promise((resolve) => { let ws; const done = () => { try { ws.close(); } catch {} resolve(); }; const timer = setTimeout(done, 25000); try { ws = new WebSocket(url); } catch (e) { clearTimeout(timer); return resolve(); } let eose = 0; ws.onopen = () => { ws.send(JSON.stringify(['REQ', 'req', { kinds, since, limit: 5000 }])); ws.send(JSON.stringify(['REQ', 'res', { kinds: resultKinds, since, limit: 5000 }])); ws.send(JSON.stringify(['REQ', 'fb', { kinds: [7000], since, limit: 5000 }])); }; ws.onmessage = (m) => { let msg; try { msg = JSON.parse(m.data); } catch { return; } if (msg[0] === 'EVENT') { const ev = msg[2]; const target = msg[1] === 'req' ? events : msg[1] === 'res' ? results : feedback; if (!target.has(ev.id)) target.set(ev.id, { ev, relays: new Set() }); target.get(ev.id).relays.add(url); } else if (msg[0] === 'EOSE') { eose++; if (eose >= 3) { clearTimeout(timer); done(); } } else if (msg[0] === 'CLOSED' || msg[0] === 'NOTICE') { console.error(url, JSON.stringify(msg).slice(0, 200)); } }; ws.onerror = () => { clearTimeout(timer); done(); }; }); } await Promise.all(RELAYS.map(scan)); const tag = (ev, name) => ev.tags.find((t) => t[0] === name); const byKind = {}; const byAuthor = new Map(); const bids = []; const amountsFb = []; for (const { ev } of events.values()) { const k = ev.kind; byKind[k] ??= { n: 0, withBid: 0, bidMsats: [], authors: new Set(), encrypted: 0 }; const b = byKind[k]; b.n++; b.authors.add(ev.pubkey); if (tag(ev, 'encrypted')) b.encrypted++; const bid = tag(ev, 'bid'); if (bid && Number(bid[1]) > 0) { b.withBid++; b.bidMsats.push(Number(bid[1])); bids.push({ kind: k, msats: Number(bid[1]), pubkey: ev.pubkey, created: ev.created_at, content: (tag(ev, 'i') || [])[1] }); } byAuthor.set(ev.pubkey, (byAuthor.get(ev.pubkey) || 0) + 1); } for (const { ev } of feedback.values()) { const amt = tag(ev, 'amount'); const st = tag(ev, 'status'); if (amt && Number(amt[1]) > 0) amountsFb.push({ msats: Number(amt[1]), status: st && st[1], p: ev.pubkey }); } const median = (a) => { if (!a.length) return null; const s = [...a].sort((x, y) => x - y); return s[Math.floor(s.length / 2)]; }; console.log('Window since', new Date(since * 1000).toISOString()); console.log('Total job requests', events.size, 'results', results.size, 'feedback', feedback.size); console.log('\nkind | requests | uniqueAuthors | withBid | medianBidSats | maxBidSats | encrypted'); for (const [k, b] of Object.entries(byKind).sort((a, c) => c[1].n - a[1].n)) { console.log(k, '|', b.n, '|', b.authors.size, '|', b.withBid, '|', b.bidMsats.length ? median(b.bidMsats) / 1000 : '-', '|', b.bidMsats.length ? Math.max(...b.bidMsats) / 1000 : '-', '|', b.encrypted); } console.log('\nTop requesters (pubkey prefix: count)'); for (const [p, n] of [...byAuthor.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12)) console.log(p.slice(0, 16), n); console.log('\nUnique requesters total', byAuthor.size); console.log('\nSample bids (up to 25):'); for (const b of bids.slice(0, 25)) console.log(b.kind, b.msats / 1000, 'sats', b.pubkey.slice(0, 12), new Date(b.created * 1000).toISOString(), String(b.content || '').slice(0, 80).replace(/\n/g, ' ')); console.log('\nFeedback events with amount (payment-required quotes) count', amountsFb.length, 'median sats', median(amountsFb.map((x) => x.msats)) / 1000); const fbProviders = new Map(); for (const f of amountsFb) fbProviders.set(f.p, (fbProviders.get(f.p) || 0) + 1); console.log('Distinct providers quoting prices', fbProviders.size); const resAuthors = new Map(); for (const { ev } of results.values()) resAuthors.set(ev.kind, (resAuthors.get(ev.kind) || 0) + 1); console.log('Results by kind', JSON.stringify(Object.fromEntries(resAuthors))); process.exit(0);