Capítulo 9
Zaps
Dale propina a cualquiera, en cualquier lugar, en segundos: cómo los pagos Lightning y los eventos de Nostr se dan la mano, y qué demuestra de verdad un recibo de zap.
Sigamos una propina desde un solo toque, pasando por un servidor de wallet, Lightning y los relays, hasta el contador ⚡ de una publicación.
Imagina a un músico callejero. Te gusta la canción, así que dejas una moneda en su sombrero. Todos los que están cerca ven caer la moneda, y el músico sonríe. Así de simple.
En internet, esa “moneda en el sombrero” siempre ha sido incómoda: las comisiones de las tarjetas
se comen las propinas pequeñas, los sitios de pago piden crear una cuenta y la plataforma se queda
con una parte. La respuesta de Nostr es el
Y aquí viene el giro que hace interesantes a los zaps: los relays nunca tocan el dinero. Un
relay solo guarda
Los cuatro participantes
- La app de quien envía: el cliente de Nostr donde tocas ⚡, conectado a tu wallet Lightning.
- El servidor de la wallet de quien recibe: la empresa o el equipo autoalojado que recibe pagos
Lightning en nombre de quien recibe. Habla
, un protocolo web sencillo para preguntar “¿cómo te pago?”.LNURL LNURL: Un conjunto de convenciones HTTP (las especificaciones LUD) que permiten a una wallet pedir a un servidor una factura Lightning nueva. - La red Lightning: donde el dinero se mueve de verdad.
- Los relays: donde se anuncia el zap, para que todas las apps puedan mostrarlo.
Dale a reproducir (o avanza con las flechas) y mira cómo Alice le hace un zap al dibujo del avestruz de Erin. Cada paso muestra exactamente qué viaja entre los participantes.
Cuatro participantes, diez pasos. Recorre cómo viaja una propina desde la app de quien envía, pasando por el servidor de la wallet de quien recibe y la red Lightning, hasta volver a los relays de Nostr como un recibo.
1. Encontrar la dirección Lightning. La app lee el perfil de Erin (kind 0). Su campo lud16 guarda una dirección Lightning que parece un correo: erin@wallet.alpha.example.
Versión en texto
- 1. Encontrar la dirección Lightning. La app lee el perfil de Erin (kind 0). Su campo lud16 guarda una dirección Lightning que parece un correo: erin@wallet.alpha.example.
- 2. Tocar la puerta de la wallet. La dirección se convierte en una URL web normal. La app le pregunta a ese servidor de wallet: “¿cómo le pago a Erin?”
- 3. La wallet dice “hablo Nostr”. La respuesta incluye allowsNostr: true y un nostrPubkey: la clave que esta wallet usará para firmar los recibos. ¡Recuérdala, la revisaremos más adelante!
- 4. Escribir y firmar una solicitud de zap. La app de Alice escribe una notita firmada: a quién se le paga, cuánto, qué publicación, un comentario y qué relays deben enterarse.
- 5. Entregar la solicitud a la wallet. La solicitud de zap NO se publica en los relays. Viaja dentro de una petición web directo a la URL de callback del servidor de la wallet.
- 6. Recibir una factura Lightning. El servidor revisa la solicitud y devuelve una factura por 2100 sats. La solicitud de zap va incrustada en la descripción de la factura, así que ambas quedan unidas.
- 7. ¡A pagar!. La wallet de Alice paga la factura por la red Lightning. El dinero se mueve en segundos, sin tocar ningún relay.
- 8. El pago se liquida. El servidor de la wallet ve su factura pagada. La preimagen secreta que se revela es el sello de “pagado” de Lightning.
- 9. La wallet publica un recibo de zap. El servidor de la wallet firma un recibo de zap con su nostrPubkey y lo envía a los relays indicados en la solicitud.
- 10. Todos ven el zap. Las apps cargan los recibos desde los relays, los verifican y los suman. La publicación de Erin ahora muestra ⚡ 2100 sats.
1. Encontrar la dirección Lightning
La app lee el perfil de Erin (kind 0). Su campo lud16 guarda una dirección Lightning que parece un correo: erin@wallet.alpha.example.
Qué viaja en este paso
{ "name": "erin" ,"display_name": "Erin" ,"about": "Artist. Drawing one weird animal per day. Zaps keep me caffeinated." ,"nip05": "erin@alpha.example" ,"lud16": "erin@wallet.alpha.example" } Paso 1: encontrar el sombrero
Para dar una propina necesitas saber dónde está el sombrero. En Nostr, está en el perfil: el evento
de erin@wallet.alpha.example, pero en realidad es un atajo para una URL web. Pruébalo:
Dirección Lightning → URL de la wallet
Una dirección Lightning lud16 es solo una forma corta de escribir una URL web. Escribe una para ver la URL que consultaría una app.
https://wallet.alpha.example/.well-known/lnurlp/erinLa app consulta esa URL y el servidor de la wallet responde con sus datos de pago. Dos campos de la
respuesta importan para los zaps: allowsNostr: true (“sí, entiendo los zaps”) y nostrPubkey: la
Bajo el capó: direcciones Lightning y LNURL Mostrar detalles
LUD-16 → LUD-06. Una dirección Lightning nombre@dominio se convierte en
https://dominio/.well-known/lnurlp/nombre (LUD-16).
La respuesta es un documento LNURL-pay (LUD-06);
{
"callback": "https://wallet.alpha.example/lnurlp/erin/callback",
"minSendable": 1000,
"maxSendable": 100000000000,
"metadata": "[[\"text/plain\",\"Zap Erin\"],[\"text/identifier\",\"erin@wallet.alpha.example\"]]",
"tag": "payRequest",
"allowsNostr": true,
"nostrPubkey": "044b2cc412cc89149bc4f38a5067db6034dbaafe20321721a5414bb7237176f4"
}Los montos están en milisatoshis (1 sat = 1000 msat). nostr-tools hace la consulta por ti:
import { nip57 } from "nostr-tools";
// metadataEvent = el kind 0 de quien recibe
const callback = await nip57.getZapEndpoint(metadataEvent); // null si !allowsNostrPasos 2–6: el papeleo
Antes de pagar, la app de Alice escribe una solicitud de zap: una notita que dice “Yo, Alice,
quiero hacerle un zap de 2.100 sats a Erin por esta publicación, con el comentario ‘Love the
ostrich! ⚡’. Por favor, anúncialo en estos relays.” Alice la
Sorpresa: esta nota no se publica en los relays. La app se la entrega directamente al servidor
de la wallet de Erin en una petición web. El servidor la revisa y luego crea una
Bajo el capó: la solicitud de zap Mostrar detalles
La solicitud de zap (kind 9734): etiquetas obligatorias: p (quien recibe) y relays (adónde
debe ir el recibo). Recomendadas pero opcionales: amount (en msat, como texto) y lnurl.
Opcionales: e (evento que recibe el zap), a (evento direccionable que recibe el zap), k (su
kind). content es un comentario opcional.
{
"kind": 9734,
"content": "Love the ostrich! ⚡",
"tags": [
["relays", "wss://relay.alpha.example", "wss://relay.gamma.example"],
["amount", "2100000"],
["p", "c71750007e42443e5ca8c1ea00babed4e8c78a9cb45dd6db3733d918e0b55deb"],
["e", "def4b79eea11b4d6c0caa6da27dedc1ff5fc274a3378c64193f21aa05125d356"]
],
"pubkey": "e550c6e92808f358382b7b8c14345344f5ef9a299529834d1c26d57b6ace44cc",
"created_at": 1735684800,
"id": "24539a56…",
"sig": "b5beae9e…"
}Se envía con un HTTP GET al callback, codificada como URI en el parámetro de consulta nostr:
<callback>?amount=2100000&nostr=%7B%22kind%22%3A9734…&lnurl=<lnurl>.
El servidor debe comprobar: firma válida, etiquetas presentes, exactamente una p, cero o una e,
una etiqueta relays, amount igual al monto de la consulta y como máximo una P. Responde
{ "pr": "<bolt11>" }: una factura con description hash cuya descripción es el JSON de la
solicitud de zap.
import { finalizeEvent, nip57 } from "nostr-tools";
const template = nip57.makeZapRequest({
event: erinsPost, // o { pubkey } para un zap a un perfil
amount: 2_100_000, // msat
comment: "Love the ostrich! ⚡",
relays: ["wss://relay.alpha.example"],
});
const zapRequest = finalizeEvent(template, aliceSecretKey);
const url = `${callback}?amount=2100000&nostr=${encodeURIComponent(JSON.stringify(zapRequest))}`;
const { pr: invoice } = await (await fetch(url)).json();Pasos 7–10: se mueve el dinero y luego llega el anuncio
La wallet de Alice paga la factura por Lightning. Los relays no ven nada de esto: es Lightning puro.
Cuando llega el pago, el servidor de la wallet de Erin escribe un recibo de zap (kind 9735), lo
firma con su nostrPubkey y lo publica en los relays que indicó Alice. Cada app que carga la
publicación de Erin encuentra el recibo, lo verifica y suma ⚡ 2.100 al contador.
Bajo el capó: el recibo del zap Mostrar detalles
El recibo de zap (kind 9735) lo firma el servidor de la wallet, no Erin ni Alice.
Etiquetas: p (quien recibe), P opcional (quien envía), e/a/k copiadas de la solicitud,
bolt11 (la factura), description (el JSON de la solicitud de zap, tal cual) y preimage
opcional. created_at debería ser el momento en que se pagó la factura.
{
"kind": 9735,
"pubkey": "044b2cc412cc89149bc4f38a5067db6034dbaafe20321721a5414bb7237176f4",
"content": "",
"tags": [
["p", "c71750007e42443e5ca8c1ea00babed4e8c78a9cb45dd6db3733d918e0b55deb"],
["P", "e550c6e92808f358382b7b8c14345344f5ef9a299529834d1c26d57b6ace44cc"],
["e", "def4b79eea11b4d6c0caa6da27dedc1ff5fc274a3378c64193f21aa05125d356"],
["bolt11", "lnbc21000n1p…"],
["description", "{\"kind\":9734,\"content\":\"Love the ostrich! ⚡\",…}"],
["preimage", "0e03fe78…"]
],
"created_at": 1735684802
}Para mostrar los zaps de una publicación, suscríbete con un filtro de NIP-01:
["REQ", "zaps", { "kinds": [9735], "#e": ["def4b79eea11b4d6c0caa6da27dedc1ff5fc274a3378c64193f21aa05125d356"] }]lnbc21000n es el monto: 21000 × n (nano-BTC, 0,1 sat) = 2.100 sats.
nip57.getSatoshisAmountFromBolt11(invoice) lo decodifica por ti.
Los zaps son públicos: el recibo, con la solicitud de zap adentro, muestra quién le hizo un zap a quién, cuánto y con qué comentario. Piensa en “una moneda en el sombrero en una calle concurrida”, no en “una transferencia bancaria privada”.
¿Puedes confiar en un zap?
Ahora viene la parte honesta. Cualquiera puede publicar un evento kind 9735: es solo un evento. Así que, antes de sumar un zap al contador, una app revisa el recibo:
- ¿La firma del recibo es válida?
- ¿Lo firmó el servidor de la wallet de quien recibe, es decir, el
nostrPubkeyque vimos en el paso 1? - ¿Contiene una solicitud de zap real y firmada?
- ¿El monto de la factura coincide con el monto que pidió Alice?
Estas comprobaciones atrapan manipulaciones e impostores. Pero mira con atención el último escenario de abajo: un servidor de wallet deshonesto puede firmar un recibo de aspecto perfecto para una factura que nadie pagó. ¡Y pasa todas las comprobaciones!
¿Puedes confiar en este recibo?
Cualquiera puede publicar un evento kind 9735. Elige un escenario y mira las comprobaciones que hace una app antes de contar un zap.
Comprobaciones
- La firma del recibo es válida — superada
- Firmado con el nostrPubkey de la wallet — superada
- La solicitud de zap incluida es un kind 9734 con firma válida — superada
- El recibo y la solicitud nombran a la misma persona destinataria — superada
- El monto de la factura coincide con el monto solicitado — superada
El recibo que se está revisando
{ "id": "7682a16d40733fd532c600fc001391fdb208a9a6037f88d1f1cad2f1694ac347" ,"pubkey": "044b2cc412cc89149bc4f38a5067db6034dbaafe20321721a5414bb7237176f4" ,"created_at": 1735684802 ,"kind": 9735 , "tags": [ [ ] , [ ] , [ ] , [ ] , [ ] , [ ] ] ,"content": "" ,"sig": "2e584640f02b15def129e281551368b07bfcd0531c652a0f15a294fa1823bf6925eda9dc2e5c23c14ceab38865e14deb2e9ba682ab62722856a706f79cb35434" } Wallet honesta: 5 de 5 comprobaciones superadas. Cuenta como zap ✓
Ese es el modelo de confianza en una frase: un recibo de zap es el proveedor de wallet de quien recibe diciendo “esto se pagó”. Demuestra que alguien pidió una factura, y la wallet responde por el resto. Normalmente no hay problema (la wallet la elige quien recibe, y mentir solo inflaría sus propios ingresos), pero por eso los contadores de zaps son señales sociales, no registros contables. Los únicos que saben con certeza que hubo un pago son quien pagó (que tiene la preimagen) y quien recibió (cuyo saldo subió).
Bajo el capó: cómo validar recibos Mostrar detalles
Validación del lado del cliente (NIP-57, Apéndice F):
- el
pubkeydel recibo DEBE ser igual alnostrPubkeydel LNURL de quien recibe; - el monto de la factura
bolt11DEBE ser igual a la etiquetaamountde la solicitud (si existe); - la etiqueta
lnurlde la solicitud DEBERÍA coincidir con el lnurl de quien recibe.
El Apéndice E añade que SHA256(description) debería coincidir con el hash de descripción de la
factura, lo que pega la factura a esa solicitud de zap exacta.
NIP-57 lo dice claramente: un recibo de zap “no es una prueba de pago”; solo muestra que alguien
obtuvo una factura. Una etiqueta preimage tampoco ayuda: la wallet que creó la factura conoce la
preimagen antes de que nadie pague.
Relacionado: los eventos pueden llevar etiquetas zap para repartir los zaps entre varias
personas (NIP-57, Apéndice G), y NIP-47
(Nostr Wallet Connect) permite que las apps hablen con tu wallet para que hacer un zap sea cosa de
un toque.
import { nip57 } from "nostr-tools";
// Descarta los recibos que fallan lo básico antes de contarlos.
const request = JSON.parse(receipt.tags.find((t) => t[0] === "description")?.[1] ?? "null");
const err = nip57.validateZapRequest(JSON.stringify(request)); // null cuando la solicitud está bien
const okSigner = receipt.pubkey === lnurlPayResponse.nostrPubkey;
const sats = nip57.getSatoshisAmountFromBolt11(receipt.tags.find((t) => t[0] === "bolt11")?.[1] ?? "");Todo en esta página son datos de demostración: wallets falsas, facturas falsas, ningún sat real. Nostr School nunca mueve dinero.
Comprobación rápida
Lo esencial
- Un zap es un pago Lightning más dos eventos de Nostr: una solicitud de zap (kind 9734) firmada por quien envía y un recibo de zap (kind 9735) firmado por el servidor de la wallet de quien recibe.
- Tu dirección Lightning vive en el campo lud16 de tu perfil kind 0 y no es más que una forma corta de escribir una URL web (LUD-16).
- La solicitud de zap viaja por HTTP al servidor de la wallet, nunca a los relays; solo el recibo se publica en los relays.
- Las apps verifican la firma del recibo, quién lo firmó (el nostrPubkey de la wallet) y el monto de la factura antes de contarlo.
- Un recibo de zap es la palabra del servidor de la wallet, no una prueba de pago: confías en el proveedor de wallet de quien recibe.