feat: clarify zap receipts and add kind 9735 anatomy slide

Rework the zap slide's step 2 to say the recipient's Lightning provider
signs the receipt with its OWN nostr key (advertised as nostrPubkey),
and add a dedicated 'Anatomy of a zap receipt' slide breaking down the
kind 9735 event: who signs it (service), who was zapped (p tag), and who
zapped (the embedded signed 9734 in description). Grounded in NIP-57 and
the lnbits/lnurlp implementation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
padreug 2026-07-22 03:09:34 +02:00
commit abbfcaf3ef
3 changed files with 88 additions and 7 deletions

View file

@ -0,0 +1,72 @@
<script setup lang="ts">
import SlideShell from './SlideShell.vue'
type Role = 'server' | 'recipient' | 'sender'
interface Line {
key: string
value: string
kind: 'string' | 'number'
comment: string
role?: Role
tag?: boolean // an entry in the event's `tags` array (vs. a top-level field)
}
const lines: Line[] = [
{ key: 'kind', value: '9735', kind: 'number', comment: 'a zap receipt' },
{ key: 'pubkey', value: '"9be0…7c"', kind: 'string', comment: 'the zapper services key — it signs, not you', role: 'server' },
{ key: 'p', value: '"3bf0c6…2a"', kind: 'string', comment: 'who was zapped (recipient)', role: 'recipient', tag: true },
{ key: 'e', value: '"0a1b2c…9f"', kind: 'string', comment: 'which note (optional)', tag: true },
{ key: 'P', value: '"82f4…d1"', kind: 'string', comment: 'who zapped (optional)', role: 'sender', tag: true },
{ key: 'bolt11', value: '"lnbc210n1…"', kind: 'string', comment: 'the invoice that was paid', tag: true },
{ key: 'description', value: '"{…9734…}"', kind: 'string', comment: 'your signed request: sender + amount + comment', role: 'sender', tag: true },
{ key: 'sig', value: '"e7c9…"', kind: 'string', comment: 'signed by the service key', role: 'server' },
]
const valueClass = { string: 'text-emerald-300', number: 'text-amber-300' }
const roleBadge: Record<Role, { label: string; class: string }> = {
server: { label: 'server', class: 'bg-violet-500/15 text-violet-300' },
recipient: { label: 'zapped', class: 'bg-sky-500/15 text-sky-300' },
sender: { label: 'zapper', class: 'bg-rose-500/15 text-rose-300' },
}
</script>
<template>
<SlideShell kicker="NIP-57 · kind 9735" title="Anatomy of a zap receipt">
<p class="mb-4 text-zinc-400">The event the provider publishes when your zap is paid:</p>
<div class="flex items-center justify-center gap-3 sm:gap-6">
<span class="select-none text-[8rem] font-thin leading-none text-violet-500/80 sm:text-[10rem]">{</span>
<div class="grid grid-cols-[auto_auto_1fr] items-center gap-x-5 gap-y-1.5 font-mono text-base sm:text-lg">
<template v-for="l in lines" :key="l.key">
<span class="justify-self-end whitespace-nowrap" :class="l.tag ? 'text-violet-300/90' : 'font-semibold text-violet-300'">
<span v-if="l.tag" class="text-zinc-600">tag </span>"{{ l.key }}"<span class="text-zinc-500">:</span>
</span>
<span class="whitespace-nowrap" :class="valueClass[l.kind]">{{ l.value }}</span>
<span class="whitespace-nowrap text-sm text-zinc-600">
<span class="text-zinc-500">// </span>{{ l.comment
}}<span
v-if="l.role"
class="ml-1 rounded px-1 text-xs font-semibold"
:class="roleBadge[l.role].class"
>{{ roleBadge[l.role].label }}</span
>
</span>
</template>
</div>
<span class="select-none text-[8rem] font-thin leading-none text-violet-500/80 sm:text-[10rem]">}</span>
</div>
<div class="mt-6 flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm">
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-violet-400" />service signs it</span>
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-sky-400" /><code>p</code> = who was zapped</span>
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-rose-400" /><code>description</code> = who zapped (your signed 9734)</span>
</div>
<p class="mt-4 text-center text-sm text-zinc-500">
Clients trust it only if <code class="text-zinc-300">pubkey</code> matches the
<code class="text-zinc-300">nostrPubkey</code> the recipients provider advertised thats what stops fake zaps.
</p>
</SlideShell>
</template>

View file

@ -29,10 +29,12 @@ import SlideShell from './SlideShell.vue'
<BadgeCheck class="h-6 w-6 text-violet-400" /> <BadgeCheck class="h-6 w-6 text-violet-400" />
<span class="text-sm font-semibold uppercase tracking-wider text-violet-400">The receipt · the Nostr part</span> <span class="text-sm font-semibold uppercase tracking-wider text-violet-400">The receipt · the Nostr part</span>
</div> </div>
<p class="text-white">Their Lightning server publishes a signed <span class="font-semibold">zap receipt</span>.</p> <p class="text-white">Their Lightning provider signs a <span class="font-semibold">zap receipt</span>.</p>
<p class="mt-2 text-base text-zinc-400"> <p class="mt-2 text-base text-zinc-400">
Once the invoice clears, the server signs a receipt proving payment and posts it to relays. The provider has its <span class="text-zinc-200">own Nostr key</span>. When the invoice clears it
Now the zap shows up <span class="text-zinc-200">publicly under the note</span> amount, sender, comment. signs a <code class="text-zinc-300">kind 9735</code> event tagged with
<span class="text-zinc-200">who was zapped</span> and embedding your signed request (which says who
zapped, how much, and any comment) then posts it to the relays you named.
</p> </p>
</div> </div>
</div> </div>
@ -47,10 +49,11 @@ import SlideShell from './SlideShell.vue'
</div> </div>
<p class="mt-4 text-sm text-zinc-500"> <p class="mt-4 text-sm text-zinc-500">
Under the hood (NIP-57): your client attaches a signed Under the hood (NIP-57): your signed
<code class="text-zinc-300">zap request (kind 9734)</code>; the recipients server returns a <code class="text-zinc-300">zap request (kind 9734)</code> rides along with the LNURL-pay invoice; the
<code class="text-zinc-300">zap receipt (kind 9735)</code>. The server must opt in via provider advertises <code class="text-zinc-300">allowsNostr</code> + its
<code class="text-zinc-300">allowsNostr</code>. <code class="text-zinc-300">nostrPubkey</code>, and clients only trust a
<code class="text-zinc-300">9735</code> receipt signed by that key. (Next: its anatomy )
</p> </p>
</SlideShell> </SlideShell>
</template> </template>

View file

@ -10,6 +10,7 @@ import OutboxSlide from '@/components/slides/OutboxSlide.vue'
import RelayCategoriesSlide from '@/components/slides/RelayCategoriesSlide.vue' import RelayCategoriesSlide from '@/components/slides/RelayCategoriesSlide.vue'
import RelayAccessSlide from '@/components/slides/RelayAccessSlide.vue' import RelayAccessSlide from '@/components/slides/RelayAccessSlide.vue'
import ZapSlide from '@/components/slides/ZapSlide.vue' import ZapSlide from '@/components/slides/ZapSlide.vue'
import ZapReceiptSlide from '@/components/slides/ZapReceiptSlide.vue'
import ResourcesSlide from '@/components/slides/ResourcesSlide.vue' import ResourcesSlide from '@/components/slides/ResourcesSlide.vue'
/** /**
@ -205,6 +206,11 @@ export const slides: Slide[] = [
label: 'How a zap works', label: 'How a zap works',
component: ZapSlide, component: ZapSlide,
}, },
{
id: 'zap-receipt',
label: 'Zap receipt anatomy',
component: ZapReceiptSlide,
},
{ {
id: 'recap', id: 'recap',
label: 'Recap', label: 'Recap',