NAME

Net::Nostr::Zap - NIP-57 Lightning Zaps

SYNOPSIS

use Net::Nostr::Zap qw(
    lud16_to_url encode_lnurl decode_lnurl
    bolt11_amount callback_url calculate_splits
);
use Net::Nostr::Key;

my $key = Net::Nostr::Key->new;

# Create a zap request (kind 9734)
my $zap_req = Net::Nostr::Zap->new_request(
    p      => $recipient_pubkey,
    relays => ['wss://relay.example.com'],
    amount => '21000',
    lnurl  => encode_lnurl('https://example.com/.well-known/lnurlp/alice'),
    e      => $event_id,
    k      => '1',
);
my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);

# Send zap request to recipient's LNURL callback
my $url = callback_url('https://lnurl.example.com/callback',
    amount => 21000,
    nostr  => $event,
    lnurl  => $zap_req->lnurl,
);

# Parse a received zap request
my $req = Net::Nostr::Zap->request_from_event($event);
say $req->p;        # recipient pubkey
say $req->amount;   # '21000'

# Create a zap receipt (kind 9735)
my $zap_receipt = Net::Nostr::Zap->new_receipt(
    p           => $recipient_pubkey,
    bolt11      => $bolt11_invoice,
    description => $zap_request_json,
    sender      => $sender_pubkey,
    e           => $event_id,
    preimage    => $preimage_hex,
);
my $receipt_event = $zap_receipt->to_event(pubkey => $server_pubkey);

# Parse a received zap receipt
my $receipt = Net::Nostr::Zap->receipt_from_event($receipt_event);
say $receipt->bolt11;
my $embedded_req = $receipt->zap_request;  # Net::Nostr::Event

# Validate a zap request (Appendix D)
Net::Nostr::Zap->validate_request($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);

# Validate a zap receipt (Appendix F)
Net::Nostr::Zap->validate_receipt($receipt_event,
    nostr_pubkey => $expected_pubkey,
);

# Convert lightning address to LNURL pay endpoint URL
my $pay_url = lud16_to_url('alice@example.com');
# https://example.com/.well-known/lnurlp/alice

# Parse bolt11 invoice amount in millisats
my $msats = bolt11_amount('lnbc10u1p3unwfu...');  # 1_000_000

# Calculate zap splits from zap tags (Appendix G)
my @splits = calculate_splits(@zap_tags);
for my $split (@splits) {
    say "$split->{pubkey}: $split->{percentage}%";
}

DESCRIPTION

Implements NIP-57 Lightning Zaps, which defines two event types for recording lightning payments between Nostr users:

  • Zap request (kind 9734) - Created by the sender and sent to the recipient's LNURL pay callback URL (not published to relays).

  • Zap receipt (kind 9735) - Created by the recipient's lightning wallet when the invoice is paid, and published to relays.

CONSTRUCTORS

new_request

my $zap = Net::Nostr::Zap->new_request(
    p       => $recipient_pubkey,   # required
    relays  => \@relay_urls,        # required
    amount  => '21000',             # millisats, recommended
    lnurl   => 'lnurl1...',        # recommended
    e       => $event_id,           # optional, if zapping an event
    a       => '30023:pk:slug',     # optional, for addressable events
    k       => '1',                 # optional, target event kind
    content => 'Great post!',       # optional message
);

Creates a zap request. p (recipient pubkey) and relays are required.

new_receipt

my $zap = Net::Nostr::Zap->new_receipt(
    p           => $recipient_pubkey,    # required
    bolt11      => $invoice,             # required
    description => $zap_request_json,    # required
    sender      => $sender_pubkey,       # optional (P tag)
    e           => $event_id,            # optional
    a           => '30023:pk:slug',      # optional
    k           => '1',                  # optional
    preimage    => $preimage_hex,        # optional
);

Creates a zap receipt. p, bolt11, and description are required. The description must be the JSON-encoded zap request event.

request_from_event

my $zap = Net::Nostr::Zap->request_from_event($event);

Parses a kind 9734 event into a Zap object. Croaks if the event is not kind 9734.

my $zap = Net::Nostr::Zap->request_from_event($event);
say $zap->p;       # recipient pubkey
say $zap->amount;  # millisats or undef

receipt_from_event

my $zap = Net::Nostr::Zap->receipt_from_event($event);

Parses a kind 9735 event into a Zap object. Croaks if the event is not kind 9735.

my $zap = Net::Nostr::Zap->receipt_from_event($receipt_event);
say $zap->bolt11;       # bolt11 invoice
say $zap->description;  # JSON zap request

METHODS

to_event

my $event = $zap->to_event(pubkey => $hex_pubkey);
my $event = $zap->to_event(pubkey => $hex, created_at => time());

Creates a Net::Nostr::Event from the zap object. Extra arguments are passed through to the Event constructor.

For zap requests, creates a kind 9734 event. For zap receipts, creates a kind 9735 event with empty content.

my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);

zap_request

my $event = $zap_receipt->zap_request;

Parses the description tag of a zap receipt back into a Net::Nostr::Event object representing the original zap request. Only available on receipt objects.

my $receipt = Net::Nostr::Zap->receipt_from_event($event);
my $req = $receipt->zap_request;
say $req->pubkey;  # the sender's pubkey

p

my $pubkey = $zap->p;

Returns the recipient's pubkey (from the p tag).

relays

my $relays = $zap->relays;  # arrayref

Returns the relay URLs (zap request only).

amount

my $msats = $zap->amount;  # '21000' or undef

Returns the amount in millisats (zap request only). This is a string.

lnurl

my $lnurl = $zap->lnurl;  # 'lnurl1...' or undef

Returns the bech32-encoded LNURL (zap request only).

e

my $event_id = $zap->e;  # hex or undef

Returns the zapped event ID.

a

my $coord = $zap->a;  # 'kind:pubkey:d-tag' or undef

Returns the addressable event coordinate.

k

my $kind = $zap->k;  # '1' or undef

Returns the stringified kind of the target event.

content

my $msg = $zap->content;

Returns the zap message (zap request) or empty string (zap receipt).

bolt11

my $invoice = $zap->bolt11;

Returns the bolt11 invoice (zap receipt only).

description

my $json = $zap->description;

Returns the JSON-encoded zap request (zap receipt only).

sender

my $pubkey = $zap->sender;  # hex or undef

Returns the sender's pubkey from the P tag (zap receipt only).

preimage

my $preimage = $zap->preimage;  # hex or undef

Returns the payment preimage (zap receipt only).

CLASS METHODS

validate_request

Net::Nostr::Zap->validate_request($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);
Net::Nostr::Zap->validate_request($event, receipt_pubkey => $pubkey);

Validates a zap request event per Appendix D of NIP-57. Croaks on validation failure. Checks:

1. Valid Schnorr signature
2. Must have tags
3. Exactly one p tag
4. Zero or one e tags
5. Should have relays tag (warns if missing)
6. amount tag must match amount parameter if both present
7. a tag must be a valid event coordinate
8. Zero or one P tags; if present and receipt_pubkey is given, the P tag value must equal receipt_pubkey
my $key = Net::Nostr::Key->new;
my $event = $zap_req->to_event(pubkey => $key->pubkey_hex);
$key->sign_event($event);
Net::Nostr::Zap->validate_request($event, amount => 21000);

validate_receipt

Net::Nostr::Zap->validate_receipt($event,
    nostr_pubkey => $expected,
    lnurl        => $recipient_lnurl,
);

Validates a zap receipt event per Appendix F of NIP-57. Croaks on validation failure. Checks:

  • Receipt pubkey must match nostr_pubkey (the recipient's LNURL server pubkey)

  • Must have p, bolt11, and description tags

  • Invoice amount must match the zap request's amount tag if present

  • If lnurl is provided and the zap request contains an lnurl tag, warns if they do not match (SHOULD per spec)

Net::Nostr::Zap->validate_receipt($receipt_event,
    nostr_pubkey => $server_pubkey,
    lnurl        => $recipient_lnurl,
);

FUNCTIONS

All functions are exportable. None are exported by default.

lud16_to_url

my $url = lud16_to_url('alice@example.com');
# https://example.com/.well-known/lnurlp/alice

Converts a lightning address (LUD-16 format) to its LNURL pay endpoint URL.

my $url = lud16_to_url('bob@pay.domain.org');
# https://pay.domain.org/.well-known/lnurlp/bob

encode_lnurl

my $lnurl = encode_lnurl('https://example.com/.well-known/lnurlp/alice');
# lnurl1dp68gurn8ghj7...

Encodes a URL as a bech32 string with the lnurl prefix.

decode_lnurl

my $url = decode_lnurl('lnurl1dp68gurn8ghj7...');
# https://example.com/.well-known/lnurlp/alice

Decodes a bech32-encoded LNURL back to a URL string. Croaks if the prefix is not lnurl.

bolt11_amount

my $msats = bolt11_amount('lnbc10u1p3unwfu...');  # 1_000_000

Extracts the amount in millisats from a bolt11 lightning invoice. Returns undef if the invoice has no amount. Croaks if the string is not a valid bolt11 invoice.

Amount multiplier suffixes:

m = milli (10^-3 BTC) = 100,000,000 millisats per unit
u = micro (10^-6 BTC) = 100,000 millisats per unit
n = nano  (10^-9 BTC) = 100 millisats per unit
p = pico  (10^-12 BTC) = 0.1 millisats per unit

bolt11_amount('lnbc20m1...')   # 2_000_000_000
bolt11_amount('lnbc2500u1...') # 250_000_000
bolt11_amount('lnbc10u1...')   # 1_000_000

callback_url

my $url = callback_url($base_callback,
    amount => 21000,
    nostr  => $zap_request_event,
    lnurl  => 'lnurl1...',
);

Constructs the HTTP GET URL for sending a zap request to the recipient's LNURL callback endpoint (Appendix B). The nostr parameter should be a Net::Nostr::Event object which will be JSON-encoded and URI-escaped.

my $url = callback_url('https://lnurl.example.com/callback',
    amount => 21000,
    nostr  => $signed_event,
    lnurl  => $lnurl,
);
# https://lnurl.example.com/callback?amount=21000&nostr=%7B...%7D&lnurl=lnurl1...

calculate_splits

my @splits = calculate_splits(@zap_tags);

Calculates zap split percentages from zap tags on an event (Appendix G). Each tag should be an arrayref: ['zap', $pubkey, $relay, $weight].

Returns a list of hashrefs with pubkey, relay, and percentage keys.

If no tags have weights, the split is equal among all recipients. If some tags have weights and others don't, the weightless tags get 0%.

# From spec example: 25%, 25%, 50%
my @splits = calculate_splits(
    ['zap', $pk1, $relay1, '1'],
    ['zap', $pk2, $relay2, '1'],
    ['zap', $pk3, $relay3, '2'],
);
say $splits[0]{percentage};  # 25
say $splits[2]{percentage};  # 50

SEE ALSO

NIP-57, Net::Nostr, Net::Nostr::Event, Net::Nostr::Key