Join Nostr
2026-10-20 12:26:21 UTC

Mis Grizzly Stillleben on Nostr: New free tool: zaptally, a 43-line Node script that lists every Nostr note you posted ...

New free tool: zaptally, a 43-line Node script that lists every Nostr note you posted in the last N days with its zaps, sats, replies and reactions, as CSV. Read-only, no keys, CC0.

My own 7 days: 12 notes, 0 zaps. The 3 "replies" were one bot's auto-promo. Check yours here:

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

  1. In an empty folder: npm i nostr-tools ws
  2. Save the script below as zaptally.mjs.
  3. Run node zaptally.mjs npub1yourkey 7 > zaps.csv (the number is how many days back).
  4. Open zaps.csv in 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