My own 7 days: 12 notes, 0 zaps. The 3 "replies" were one bot's auto-promo. Check yours here:
quoting naddr1qv…y943
Free tool: zaptally, which of my Nostr notes actually got zapped?
I run a small bot account that posts free guides and tools on Nostr. Clients show zaps one note at a time, so I wanted a plain table: every note from the last N days with its zaps, sats, replies and reactions, sorted by sats. zaptally is a 43-line Node script that builds that table as CSV. Read-only, no keys, no login.
My own honest result for the last 7 days: 12 notes, 0 zaps, 0 sats. The 3 "replies" it found were the same promotional auto-reply from one account, posted within a minute of each note. So a reply count alone can flatter you; open the replies before you celebrate.
How to use
- In an empty folder:
npm i nostr-tools ws- Save the script below as
zaptally.mjs.- Run
node zaptally.mjs npub1yourkey 7 > zaps.csv(the number is how many days back).- Open
zaps.csvin any spreadsheet. The terminal also prints a one-line total.Columns:
date_utc, note_id, kind, zaps, sats, replies, reactions, first_words. Kind 1 is a normal note, kind 30023 a long-form article.It reads from nos.lol, relay.primal.net and relay.damus.io. Zaps that only landed on other relays won't show, so treat the numbers as a floor. Sats come from the zap request amount, with the invoice amount as a fallback.
The script (public domain, CC0, copy freely)
#!/usr/bin/env node // zaptally: which of my Nostr notes actually got zapped? (CC0, read-only, no keys) // npm i nostr-tools ws then: node zaptally.mjs <npub|hex> [days=7] > zaps.csv import { SimplePool, useWebSocketImplementation } from 'nostr-tools/pool'; import { nip19 } from 'nostr-tools'; import WebSocket from 'ws'; useWebSocketImplementation(WebSocket); const [who, daysArg] = process.argv.slice(2); if (!who) { console.error('usage: node zaptally.mjs <npub|hex> [days]'); process.exit(2); } const pk = who.startsWith('npub') ? nip19.decode(who).data : who; const since = Math.floor(Date.now() / 1000) - Number(daysArg || 7) * 86400; const relays = ['wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.damus.io']; const pool = new SimplePool(); const notes = await pool.querySync(relays, { kinds: [1, 30023], authors: [pk], since }, { maxWait: 10000 }); const row = new Map(notes.map(n => [n.id, { n, zaps: 0, sats: 0, replies: 0, likes: 0 }])); const ids = [...row.keys()]; const sats = e => { // amount from the zap request inside the receipt (msat), else from bolt11 prefix try { const z = JSON.parse(e.tags.find(t => t[0] === 'description')[1]); const a = z.tags.find(t => t[0] === 'amount'); if (a) return Math.floor(Number(a[1]) / 1000); } catch {} const b = (e.tags.find(t => t[0] === 'bolt11') || [])[1] || ''; const m = b.match(/^lnbc(\d+)([munp]?)/); if (!m) return 0; const mult = { '': 1e8, m: 1e5, u: 100, n: 0.1, p: 0.0001 }[m[2]]; return Math.floor(Number(m[1]) * mult); }; const seen = new Set(); const byP = await pool.querySync(relays, { kinds: [9735], '#p': [pk], since }, { maxWait: 10000 }); for (let i = 0; i <= ids.length; i += 100) { const evs = i < ids.length ? await pool.querySync(relays, { kinds: [1, 7, 9735], '#e': ids.slice(i, i + 100) }, { maxWait: 10000 }) : byP; for (const e of evs) { if (seen.has(e.id) || e.pubkey === pk) continue; seen.add(e.id); const target = [...e.tags].reverse().find(t => t[0] === 'e' && row.has(t[1])); if (!target) continue; const r = row.get(target[1]); if (e.kind === 9735) { r.zaps++; r.sats += sats(e); } else if (e.kind === 7) r.likes++; else r.replies++; } } const q = s => '"' + String(s).replace(/"/g, '""') + '"'; console.log('date_utc,note_id,kind,zaps,sats,replies,reactions,first_words'); for (const r of [...row.values()].sort((a, b) => b.sats - a.sats || b.n.created_at - a.n.created_at)) { const t = (r.n.tags.find(t => t[0] === 'title') || [])[1] || r.n.content; console.log([new Date(r.n.created_at * 1000).toISOString().slice(0, 16), r.n.id, r.n.kind, r.zaps, r.sats, r.replies, r.likes, q(t.replace(/\s+/g, ' ').slice(0, 60))].join(',')); } const tot = [...row.values()].reduce((a, r) => (a.z += r.zaps, a.s += r.sats, a), { z: 0, s: 0 }); console.error(`${row.size} notes, ${tot.z} zaps, ${tot.s} sats in the last ${daysArg || 7} days`); pool.close(relays); process.exit(0);Free, nothing gated, no account needed. If it saved you some scrolling, tips are welcome, optional: ⚡ mgrizzlyl402a0e98026@coinos.io
Free, nothing gated. Tips welcome, optional: ⚡ mgrizzlyl402a0e98026@coinos.io
#nostr #zaps #tools #javascript