When someone tips an idea <3, the receipt is a NIP-57 zap on a version of that idea. Edit the idea and the next receipt lands on a new event id. The coordinate stays; the money trail does not. Indexers that key on e tags then report a smaller total than the author actually received, and a later maintainer cannot prove the first month of funding.
Bitspark already tips the coordinate (#a on 30023:<pubkey>:<d>). That is the right target. What is missing is a receipt the author can keep, independent of any indexer, that still verifies on a relay that has forgotten the original zap.
A tip that cannot be shown later is a donation the network pretends never happened.
What we are building#
Idea Receipts: a small, signed, addressable document per tip (or per day of tips) that any client can fetch by the idea's coordinate and verify without trusting Bitspark.
Three parts, each a separate job under this idea:
- Witness — watches zap receipts that tag the idea
a-tag, checks the invoice preimage, and writes one JSON line per valid tip. - Ledger — aggregates witnesses into one parameterized replaceable event per idea per UTC day.
- Card — a dependency-free renderer: one line in the idea rail ("12 400 sats · 7 receipts") and a printable page for grant applications.
1. Witness#
The witness is a single Node process. It does not hold keys that can spend. It only signs observations:
receipts witness --idea 30023:<pubkey>:<d> --out ./days/
# zap 9735 e=ab12… amount=21000 ok
# zap 9735 e=cd34… amount=500 skipped (invoice mismatch)
# flush 2026-09-06.jsonl 3 receipts 42 100 sats
Each line is one verified NIP-57 receipt. Lines are append-only. A bad witness can lie; two independent witnesses that agree are enough for the card to display a number.
2. Ledger — one event per idea per day#
{
"kind": 30078,
"tags": [
["d", "30023:<pubkey>:<d>:2026-09-06"],
["a", "30023:<pubkey>:<d>"],
["amount", "42100"],
["count", "3"],
["witness", "<hex pubkey>"]
],
"content": "{\"receipts\":[{\"id\":\"ab12\",\"sats\":21000}]}"
}
Kind 30078 is a parameterized replaceable application-specific event. The d tag is the idea address plus the UTC date, so a re-publish the same day replaces the day, it does not fork it. Older days stay.
3. Card#
import { tallyIdea } from '@bitspark/receipts';
const t = await tallyIdea({
address: '30023:<pubkey>:<d>',
witnessPubkeys: RECEIPT_WITNESSES,
maxAgeSeconds: 86_400
});
// t.sats, t.count, t.days — nothing else
No NDK in the card. It must run in the Bitspark SPA, in a Cloudflare Worker, and in a static print page.
Why this is not just "look at the zap indexer"#
| Failure | Indexer | Idea receipts |
|---|---|---|
| Idea is edited | totals reset if keyed on e |
totals stay on the a-tag |
| Indexer goes offline | rail shows 0 | last signed day still on relays |
| Author wants a PDF for a grant | scrape HTML | one page, signature block, dates |
| Two clients disagree | pick an indexer | require n matching witness pubkeys |
The earlier work on Quoted article is the same shape applied to relays: measure, sign, let clients decide. Receipts are that shape applied to money.
Milestones and money#
Each milestone becomes a job after this draft is planted. Budgets are estimates.
| # | Milestone | Deliverable | Budget | State |
|---|---|---|---|---|
| 1 | Witness v1 | CLI, fixture zap receipts, JSONL schema | 280 000 sats | first job |
| 2 | Ledger | kind 30078 per idea per day, hourly republish |
220 000 sats | after 1 |
| 3 | Card | @bitspark/receipts on npm, SPA rail + print page |
260 000 sats | after 2 |
| 4 | Bitspark rail | replace the live indexer number with the card | 120 000 sats | after 3 |
Tips on this idea go 70 / 30 to the author and a listed maintainer. First they pay two small VMs for witnesses; then they pay the jobs.
What this is not#
- Not a wallet. Receipts never move sats. They only say that sats moved.
- Not a replacement for NIP-57. Zaps stay zaps. We archive what the protocol already emits.
- Not a Bitspark-only format. Any client that can read
30078and check a witness pubkey can show the same number. - Not live balances. A day closes at 00:00 UTC. Intraday numbers are the witness JSONL, not the ledger event.
Open questions#
- Who may witness? Anyone, or a short allow-list the idea author publishes? Allow-list is simpler to display; open witnessing is harder to fake as a group.
- Split tips. A NIP-57 zap with
zaptags already splits. Does one receipt per split-output, or one receipt for the parent zap? - Retention. A 30078 event per idea per day is small. Still: do we roll weeks into a single month event after 90 days?
Maintainer for jobs and polls: npub1389gv…v4r5.
How to help#
- Review the draft. This is unpublished. Comments belong in the thread once it is planted.
- Build. Milestone 1 is scoped so someone who has parsed a
9735before can finish it in a week. - Fund. After publish, tip the idea. The split is on the event, not in this markdown.
Status is building: the protocol is sketched, no code yet, looking for one more client that will read 30078 so the card is not Bitspark-only.

Comments
No comments yet. Start the conversation.