# Zapf Docs > Zap anyone, anywhere This file contains all documentation content in a single document following the llmstxt.org standard. ## The Fallback Address Lightning Network payments are push-based. Traditionally, the recipient must generate an invoice *before* a payment can occur. If the recipient doesn't have a wallet—or their node is temporarily offline—the sender's payment fails. The **Fallback Address** solves this friction. It guarantees that payments never fail, safely holding funds in a temporary escrow until the recipient is ready to claim them. --- ## For Users: How Your Funds Stay Safe You don't need a Lightning wallet to start receiving zaps. If someone sends you value across the network, the Fallback Address automatically catches it. ### The Experience 1. **Safe Escrow**: A trusted Zap Settlement Provider (ZSP) receives the funds and places them in a secure temporary hold on your behalf. 2. **Verifiable Proof**: The ZSP publishes a public cryptographic receipt, proving to the sender that the payment succeeded. 3. **The Claim Flow**: Whenever you are ready, you connect your personal wallet (via Nostr Wallet Connect). The ZSP instantly and automatically sweeps all pending funds to your personal balance. Once your wallet is connected, the Fallback Address retires. All future payments will settle natively and directly into your own wallet. ### Receiving Limits & Provider Policies Because the Zap Settlement Provider temporarily holds these funds on its own Lightning nodes, they must manage channel liquidity. To ensure system stability, individual ZSPs typically enforce daily or monthly caps on how much volume a Fallback Address can receive. If you anticipate receiving significant volume—or want to bypass these limits entirely—we strongly recommend connecting your own native Lightning wallet. Always review your specific ZSP's terms of service regarding escrow limits. --- ## For Developers: Implementing the Fallback Flow The Fallback mechanism allows your application to support identity-based payments for users who haven't fully onboarded yet, removing the risk of failed payments hurting your UX. ### Triggering the Fallback (`pubkey@zsp_domain`) When your frontend application resolves a user's identity but discovers they lack a valid `lud16` (native Lightning Address), you must construct an LNURL pointing to the ZSP's fallback handler. The format strictly expects the recipient's **64-character hex Nostr public key**, joined with the ZSP's domain. ```typescript // Example: Constructing the Fallback LNURL Address const fallbackAddr = `${pubkey}@zapf.app`; // Result: a1b2c3...f8e9@zapf.app ``` This deterministic format ensures the ZSP knows exactly *who* the funds belong to when creating the pending balance. *(Note: Do not use `npub` bech32 formats; the backend resolver exclusively requires hex).* ### Behind the Scenes: The ZSP Lifecycle When a payment is sent to a `pubkey@zsp_domain` address, the ZSP manages the transaction state: 1. **Internal Settlement**: The ZSP generates an invoice from its own Master Node and settles the payment internally into a custodial ledger. 2. **Cryptographic Proof**: The ZSP publishes a [Zap Receipt (Kind 5521)](/protocol/zap-receipt-5521), providing finality to the sender and an immutable record of the pending liability. 3. **Resolution**: To retrieve their funds, the recipient completes the identity verification flow and publishes an [Identity Connection (Kind 35521)](/protocol/identity-connection-35521). 4. **The Sweep**: The ZSP detects the new `35521` event, verifies the signature ownership, and automatically sweeps the Escrow balance to the user's newly designated node. --- ## Identity Authority (IA) An **Identity Authority (IA)** is a service that verifies a user's ownership of a legacy account (like Discord, X, or Email) and issues cryptographic proof of that ownership on the Nostr network. ## Role of an IA An IA bridges the gap between traditional social platforms and the permissionless Nostr network. When a user wants to link their Discord account to their Nostr public key, they cannot simply claim it — other network participants need proof. An IA provides this by: 1. Facilitating the identity verification flow (bot-assisted challenge for Discord/Telegram, or direct challenge-token for web-based LIDPs) with the provider. 2. Recording proof of the verification (the public post URL and challenge token). 3. Signing a cryptographic [attestation](/protocol/ia-attestation-35522) and publishing it to the network. ## Trust Model The protocol operates on a **Federated Trust Model**: - Zapf is an **open protocol** — anyone can run an independent IA server. There is no central gatekeeper. - Wallet clients and apps choose which IAs they trust to accurately verify identities. For example, if an independent developer runs a "Community IA", users of a different wallet could configure it to trust that Community IA. ## Multi-IA Stacking By linking verifications from multiple sources (like Discord and X), users build an un-spoofable, portable identity that stays with them across the entire internet. A user's [Identity Connection](/protocol/identity-connection-35521) can reference proofs from multiple different IAs via `e` tags, all attesting to the same legacy account. This provides resilience: if one IA goes offline or is compromised, the identity remains verifiable via the others. Verification evidence is stored inside the Kind 35522 `evidence` tag. Since verification is backed by a publicly accessible URL (the post the user made), any IA can independently re-verify by fetching that URL — no user interaction required. ## IA Responsibilities To be a fully compliant IA, the service must: - Maintain high uptime for its public relay. - Actively manage revocations if an identity changes hands or expires. When revoking an `active` session, also remove the `Identity` routing record from the store. - Enforce strict identifier normalization (e.g., lowercasing emails) to prevent spoofing. - Communicate the activation requirement to users: publishing Kind 35522 and returning it to the client does **not** activate routing. Routing only becomes live after the user signs and publishes Kind 35521 **and** sends the activation signal to the IA. The IA must expose an activation mechanism for this purpose; the specific interface is implementation-defined. If the user declines or abandons the signing dialog without completing activation, no routing link is created and the session is cleaned up. --- ## Legacy Identity Providers (LIDP) A **Legacy Identity Provider (LIDP)** is any traditional identity or social platform that a Zap Settlement Provider (ZSP) uses to verify a user's ownership of an account. As an **open protocol**, Zapf allows developers to integrate new LIDPs. By binding these accounts to Nostr public keys, anyone with a Discord, Telegram, X, or GitHub account can receive Lightning payments — even if they've never interacted with Nostr or Lightning before. ## Supported LIDPs | LIDP | Stable Identifier | Verification Method | |------|-------------------|---------------------| | **Discord** | Username (e.g. `loki_nakamo`) | Bot Challenge — user runs `/nostr-verify ` in a channel; posts `npv1` challenge publicly | | **Telegram** | @username (e.g. `@loki_nakamo`) | Bot Challenge — user runs `/nostr_verify ` in a channel; posts `npv1` challenge publicly | | **X (Twitter)** | `@handle` | Challenge Token — user posts a message containing the code | | **GitHub** | `@username` | Challenge Token — user creates a Gist containing the code | | **Instagram** | `@username` | Challenge Token — user puts the code in their bio | | **Facebook** | Profile URL | Challenge Token — user posts the code on their profile | | **Domain** | Domain name | Challenge Token — user adds a TXT DNS record | | **Email** | `user@example.com` | OTP — ZSP sends a one-time code to the inbox | The **stable identifier** (not the display name) is used as the handle in zap receipts. This means the displayed name stays consistent even if the user changes their display name on the platform. ## The Privacy Model Every connection is identified by a **hashed ConnectionKey** — the raw identifier (like your Discord user ID or email) is never published to the network in the `d` tag itself. However, the Kind 35522 `evidence` tag always contains `user_id` and `username` in cleartext and is publicly readable on the relay. Profile metadata in the Kind 35521 `content` field is also always public JSON. There is no private mode for identity connections. ## Verification Methods LIDPs use two verification approaches: ### 1. Challenge Token (X, GitHub, Instagram, Facebook, Domain, Discord, Telegram) All supported providers (except email) use the same public-proof model: the user publicly posts an [`npv1` challenge token](/protocol/ia-attestation-35522#the-npv1-challenge-token) — a bech32-encoded, session-bound proof — and submits the public URL as evidence. **Direct** (X, GitHub, Instagram, Facebook, Domain): 1. The user receives the `npv1` token from the Zapf web app. 2. The user publishes it on their account (tweet, gist, bio, DNS TXT record, etc.). 3. The user submits the link; the ZSP fetches the content, checks the token is present, and issues a [Kind 35522 Attestation](/protocol/ia-attestation-35522). **Bot-assisted** (Discord, Telegram): 1. The user receives a pre-auth code from the web app. 2. The user runs `/nostr-verify ` (Discord) or `/nostr_verify ` (Telegram) in a community channel. 3. The bot responds with the `npv1` challenge and binds the session on the IA. 4. The user posts the challenge publicly in the same channel and submits the message link. 5. The ZSP verifies the public post and issues a [Kind 35522 Attestation](/protocol/ia-attestation-35522). The bot automates challenge issuance and link-checking; the underlying proof model (public `npv1` post + `evidence_url`) is identical to the direct flow. ### 2. OTP (Email) The ZSP sends a one-time code to the user's inbox. The user submits the code to prove ownership. The ZSP issues the attestation immediately upon correct submission. --- ## Verification Model When a client application (like a wallet or a social client) parses a [Proxy Request (Kind 5523)](/protocol/on-behalf-zap-request-5523) or its resultant [Zap Receipt (Kind 5521)](/protocol/zap-receipt-5521), it needs to resolve the legacy identifier into a native Nostr identity to display a "Verified" rich profile. This protocol implements a deterministic, network-based verification model to balance **speed** and **security**. ## The Identity Lookup The verification flow relies on querying the Nostr network for an [Identity Connection (Kind 35521)](/protocol/identity-connection-35521) that binds the target identifier hash to a Nostr Public Key. **How it works:** 1. The client extracts the 3-element identity tag (e.g., `["p", "", "discord"]`). 2. It constructs a filter query targeting the Nostr network (and specifically the ZSP's relays): ```json { "kinds": [35521], "#d": [""] } ``` 3. The client receives the 35521 event. 4. (Optional but Recommended) The client fetches the [Kind 35522](/protocol/ia-attestation-35522) from the relay URL in the `e` tag to confirm a trusted Identity Authority (IA) attested to this linkage. If the relay returns the event and it is not expired, the connection is considered **Verified**. ## Verification States Clients should map the outcome of this query to clear UI indicators: | State | Outcome | Recommended UI | |-------|---------|----------------| | **Verified** | Valid `35521` event found, mapping the hash to a Nostr Key. | ✅ Native Nostr Profile | | **Pending (Fallback)** | No `35521` event found. | ⏳ Fallback LIDP Profile (e.g., Discord Avatar fetched from the ZSP API) | | **Spoofed** | `35521` found, but the Kind 35522 fetched from the `e` tag relay is invalid, missing, or revoked. | ⚠️ Red Warning ("Unverified / Suspicious") | By following this model, clients ensure that users are only shown verified identities when cryptographic proof has been surfaced, falling back gracefully to custodial profiles when the user hasn't completed their Claim Flow. --- ## Zap Settlement Provider (ZSP) The **Zap Settlement Provider (ZSP)** acts as the **Payment Processor** within the Zap Protocol. While the [Identity Authority (IA)](/concepts/identity-authority) serves as a "Bank" verifying *who* you are, the Zap Settlement Provider focuses strictly on liquidity, routing, and final settlement on the Lightning network using energy-backed coins like Bitcoin or Flokicoin. ## What is a ZSP? The **Zap Settlement Provider (ZSP)** acts as the financial engine of the Zap Protocol. If you think of an [Identity Authority](/concepts/identity-authority) as a driver's license bureau that verifies *who* you are, the Zap Settlement Provider is the specialized payment processor that actually moves your money. ZSPs work entirely behind the scenes to process payments, bridge networks, and generate the final receipts that make a global, cross-platform tipping experience possible. ## Core Responsibilities A high-quality ZSP handles the heavy lifting of payment orchestration so that users don't have to: ### 1. Payment Processing When you attempt to tip someone—whether they are on Nostr, Discord, or X—the ZSP automatically generates the lightning invoice that you pay. It safely handles the incoming funds from your wallet. ### 2. Settling Funds Once the ZSP receives your payment, it immediately forwards that value to the recipient's personal Lightning node or [Nostr Wallet Connect (NWC)](/ecosystem/supported-wallets) setup. ### 3. Publishing the Receipt After the payment successfully lands in the recipient's wallet, the ZSP publishes a cryptographic receipt to the Nostr network. This receipt is irrefutable proof that the payment occurred, and is what allows social applications to display a "Zapped!" animation on the screen. ### 4. Holding Fallback Funds Occasionally, someone might receive a tip before they have ever set up a Lightning wallet computationally. In these cases, the ZSP acts as a temporary custodian. It holds the funds in a secure [Fallback Address](/concepts/fallback-address) until the user logs in and links their wallet, at which point the ZSP automatically sweeps the funds to them. ## An Open Ecosystem The Zap Protocol was designed to be entirely permissionless and extensible: - Anyone with liquidity and node infrastructure can operate their own ZSP. - Users are free to choose any trusted ZSP to handle their payment orchestration, fostering healthy competition. --- ### Ready to Build? If you are a developer looking to build a client or deploy your own ZSP infrastructure, please refer to the technical specifications: - **[Building a ZSP (Adding a LIDP)](/guides/adding-lidp)** - **[Zap Request Specs (Native & Proxy)](/protocol/zap-request-5520)** - **[Zap Receipt Specs (Kind 5521)](/protocol/zap-receipt-5521)** --- ## Flokicon **Flokicon** is the native currency and high-speed settlement layer deeply integrated into the open Zap Protocol network. While the Zap Protocol natively supports Bitcoin over the Lightning Network, its open architecture enables multi-currency routing. Flokicon operates as a parallel, specialized currency for low-friction, high-velocity micro-transactions. ## Denominations To facilitate precise micro-tipping over Lightning, Flokicon employs a standard denomination hierarchy: | Unit | Sub-unit Equivalent | |------|---------------------| | `1 loki` | 1 Flokicon | | `1 mloki` | 0.001 loki (Milli-loki) | *The Zapf Master Node routes and settles transactions denominated down to the `mloki` level.* ## Lightning Network Integration Flokicon is natively supported within Zapf's LUD-06 and LUD-16 LNURL flows. When a user configures Flokicon as their preferred receiving currency, the Zap Settlement Provider (ZSP) automatically injects a specialized routing metadata flag into their LNURL response structure. This signals to multi-chain aware Nostr Wallet Connect integrations that they should generate a Flokicoin Lightning Invoice (`lnfc1...`) instead of standard Bitcoin (`lnbc1...`), allowing the transaction to settle on the Flokicon rails instantly with near-zero fees. --- ## Discord The Discord Legacy Identity Provider (LIDP) integration allows users to bind their Discord account to their Nostr public key. This lets anyone receive Lightning payments via their Discord identity. ## Identifier Format The canonical identifier for Discord is the user's numeric **Snowflake ID**, not their username or display name. - **Raw Format**: `discord_id` (e.g., `123456789012345678`) - **Resolved URI**: `discord:123456789012345678` *Note: While `zapf.app` allows users to search by human-readable usernames in the UI, the underlying protocol strictly routes and hashes the numeric ID to prevent issues when users change their display names.* ## Privacy Model: Public Discord is treated as a **Public** identity within the Zapf ecosystem. Because Discord profiles are generally public, users mapping their Nostr keys to Discord typically intend to be searchable. In the user's **[Identity Connection](/protocol/identity-connection-35521)** event, the `content` field always contains cleartext JSON — including `user_id` (the Discord snowflake ID), `username`, and available profile metadata such as `display_name` and `picture`. The connection key in the `d` tag is a SHA-256 hash of the identifier, not the identifier itself. ## Verification Flow (Bot-Session) Identity Authorities verify Discord ownership using a bot-session challenge. The user runs the IA bot command in a public Discord channel, which posts a challenge token. The user submits the message link to confirm they control the account. 1. **Bot command**: User runs the IA bot command in a public channel. 2. **Challenge**: The bot posts a `zapf:`-prefixed challenge token on their behalf. 3. **Submission**: User submits the public message link to the IA. 4. **Verification**: The IA fetches the message URL via the Discord API and confirms the challenge token and message author match the session. ## Expected Profile Resolution When a client resolves a Discord identity, the Identity Authority fetches basic metadata (like `username`, `global_name`, and `avatar`) from the Discord API. This data is returned to the user's Nostr client so it can render a rich "Verified" profile before the payment is sent. ## Evidence Sharing Support Because verification is backed by a publicly accessible Discord message URL, any IA can independently re-verify by fetching that URL and confirming the challenge token is present — without the user needing to re-authenticate. See [Evidence Sharing](/guides/evidence-sharing). --- ## Email Email is the most widely used Legacy Identity Provider (LIDP). It lets anyone receive zaps using a standard email address. ## Identifier Format Emails must be rigorously normalized before hashing to ensure consistent ID generation across clients and providers. 1. Stripped of leading/trailing whitespace. 2. Fully lowercased. 3. Depending on the ZSP's strictness, "plus-addressing" aliases (e.g., `user+spam@gmail.com`) may be resolved to the base address. - **Raw Format**: `user@example.com` - **Resolved URI**: `email:user@example.com` ## Privacy Model: Hashed (Strict) Email addresses are extremely sensitive Web2 identifiers. Zapf treats Email as a **High Privacy** identity. The raw email address is **never** published in the profile content of an [Identity Connection](/protocol/identity-connection-35521) event. The unique connection key is strictly a hash, meaning a user can only be discovered and zapped if the sender already knows their exact email address. ## Verification Flow (OTP) Zap Settlement Providers verify Email ownership using a standard **One-Time Password (OTP)** flow. The ZSP sends a short-lived numeric or alphanumeric code to the user's inbox, and the user enters it to prove ownership. ### No Evidence Sharing :::warning Because Email verification relies entirely on an ephemeral OTP flow controlled locally by the ZSP, **it produces no portable Access Token.** ::: This means that [Evidence Sharing](/guides/evidence-sharing) is impossible for Email identities. If a user proves their Email to `zapf.app` and receives an attestation, and later wants to use `loki.ltd` as their ZSP, they must execute a brand new OTP verification flow on the `loki.ltd` dashboard. The [IA Attestation](/protocol/ia-attestation-35522) will still exist, but the `evidence` tag will only contain the base fields (`user_id`, `username`, `verified_at`) — it will not have `evidence_url`, `challenge`, or `pre_auth_code` since there is no public post to reference. Cross-IA re-verification is therefore impossible for email identities. --- ## Other Providers As an open protocol, Zapf is designed to be extensible. Independent Identity Authorities (IAs) can implement any number of Legacy Identity Providers (LIDPs) to support niche communities or enterprise users. Common targets include: - **GitHub** (`github`) - **Google** (`google`) - **LinkedIn** (`linkedin`) - **Reddit** (`reddit`) ## Standardization Rules If an IA operator adds a new provider, they must adhere to the standard Zap Protocol conventions. ### 1. Unified Prefixing The `lidp` name MUST be short, lowercased, and used as a prefix before the identifier to create the raw string. Example: `github:flzpace` ### 2. Immutable Identifiers The chosen identifier must be stable. For GitHub, the username is changeable, so an IA *should* use the numeric user ID (e.g., `12345`) as the identifier to prevent account takeover vectors, even if the client UI resolves handles for display purposes. ### 3. Verification Mode All providers must use one of the two supported verification flows: - **Bot Session**: The IA's bot exchanges a challenge code with the user in a platform channel (Discord, Telegram). The resulting `npv1` challenge token is posted in a public message. - **Challenge Token**: The user posts the `npv1` challenge token in a publicly accessible profile field, post, or DNS record, then submits the URL (X, GitHub, TikTok, domain, etc.). In both cases, `auth_type` in the evidence JSON is always `"public_post"`. For platform-based providers where the user controls a public profile field (GitHub bio, LinkedIn About, Reddit bio), the Challenge Token flow is standard. ### 4. Evidence The `evidence` tag in the resulting [Kind 35522 Attestation](/protocol/ia-attestation-35522) must include `evidence_url` (the public post/profile URL), `challenge` (the `npv1` token that appeared there), and `pre_auth_code` (the raw code used to generate the challenge). All three are required for [cross-IA cryptographic verification](/protocol/ia-attestation-35522#cross-ia-challenge-binding). --- ## Phone (RCS) The Phone Legacy Identity Provider (LIDP) links a phone number to a Nostr public key so users can receive Lightning payments via text messages or messaging apps. ## Identifier Format Phone numbers must be strictly normalized to the international **E.164** standard. - **Rules**: Must begin with a `+`, followed by the country code, followed by the subscriber number, with all spaces, dashes, and parentheses removed. - **Raw Format**: `+12345678901` - **Resolved URI**: `phone:+12345678901` ## Privacy Model: Hashed (Strict) Like Email, Phone numbers are highly sensitive personally identifiable information (PII). Zapf treats Phone as a **High Privacy** identity. The raw identifier is stripped from all public Nostr events. The [Identity Connection](/protocol/identity-connection-35521) Connection Key is exclusively a hash of the URI string. Senders must know the recipient's phone number to zap them. ## Verification Flow (RCS OTP) Zapf Zap Settlement Providers recommend using modern **RCS (Rich Communication Services)**, or fallback SMS, to deliver a One-Time Password (OTP) code to the user's device. Upon entering the 6-digit code into the ZSP's dashboard, the ZSP issues the [IA Attestation](/protocol/ia-attestation-35522). ### Security and Evidence Similar to the Email flow, Phone verification generates an ephemeral, local proof of ownership. - **No Evidence Sharing**: Phone OTP verification is not backed by a public URL, so cross-IA re-verification is not possible. Each IA must independently deliver and verify an OTP. - **Independent Verification**: If a user wants attestations from multiple ZSPs, each ZSP must independently send and verify an RCS OTP code to that phone number. --- ## Telegram Telegram integration within the Zapf ecosystem provides two primary functions: 1. **Identity Verification**: Linking a Telegram account to a Nostr pubkey via the Identity Authority (IA). 2. **In-App Tipping**: Sending and receiving tips directly within Telegram via the Zap Telegram Bot. ## Protocol Implementation ### Verification Flow (Bot-Session) Identity Authorities verify Telegram ownership using a bot-session challenge. The user runs the IA bot command in a public Telegram group, which posts a challenge token. The user submits the message link to confirm they control the account. 1. **Bot command**: User runs the IA bot command in a public group. 2. **Challenge**: The bot posts a `zapf:`-prefixed challenge token on their behalf. 3. **Submission**: User submits the public message link to the IA. 4. **Verification**: The IA fetches the message URL via the Telegram API and confirms the challenge token and message author match the session. * **User identifier**: Stable numeric Telegram User ID (`user_id`). * **Resolved fields**: `username`, display name, profile photo. ### Evidence Sharing Support Telegram **supports Evidence Sharing**. Because verification is backed by a publicly accessible Telegram message URL, any IA can independently re-verify by fetching that URL and confirming the challenge token is present — without the user needing to re-authenticate. See [Evidence Sharing](/guides/evidence-sharing). ## Zap Telegram Bot The Zap Telegram Bot acts as a **ZSP Proxy Agent**. It represents Telegram users on the Nostr network by signing **Kind 5523 (Proxy Agent Zap Request)** events on their behalf. ### Supported Commands | Command | Description | | :--- | :--- | | `/zap @user ` | Initiates a zap to another Telegram user. | | `/wallet` | Displays the user's deterministic Lightning Address and claim link. | | `/help` | Provides information about the Zap protocol and the bot. | ### Technical Architecture The bot is implemented using the `gopkg.in/telebot.v3` library and follows the standard `Provider` interface within the `zapf-bot` project. It utilizes the same agent-namespaced BoltDB storage as the Discord provider to track pending zaps and observe NIP-57 receipts. ## Security Considerations - **Bot Verification**: Verification is tied to a specific Telegram Bot created via `@BotFather`. The bot must be a member of the group where challenges are posted. - **ConnectionKeys**: Telegram ConnectionKeys are generated using the stable `sub` (User ID) provided by Telegram, ensuring that handle changes do not break identity links. --- ## X (Twitter) The X (formerly Twitter) Legacy Identity Provider (LIDP) integrates one of the web's largest public social graphs into the open Nostr and Lightning ecosystem. ## Identifier Format The canonical identifier for X is the user's **handle** (username), stripped of the `@` symbol and lowercased. - **Raw Format**: `handle` (e.g., `jack`) - **Resolved URI**: `x:jack` ## Privacy Model: Public Like Discord, X is treated as a **Public** identity. In the user's **[Identity Connection](/protocol/identity-connection-35521)** event, the `content` field always contains cleartext JSON — including `username` (the X handle), `user_id`, and available profile metadata. The connection key in the `d` tag is a SHA-256 hash of the handle, enabling global search and discovery across Zapf clients without exposing the hash pre-image directly. ## Verification Flow (Challenge-Token) Identity Authorities verify X ownership using a public challenge-token post. 1. **Challenge**: The IA issues a short `zapf:`-prefixed token. 2. **Post**: User posts the token in their bio, a tweet, or any publicly accessible profile field. 3. **Submission**: User submits the public URL of the post to the IA. 4. **Verification**: The IA fetches the URL and confirms the token is present and the post belongs to the claimed X handle. ## Expected Profile Resolution When a client resolves an X identity, the Identity Authority fetches standard metadata (such as the username, display name, and profile picture). This enriches the native Nostr client UI, allowing users to verify they are zapping the correct X personality. ## Evidence Sharing Support Because verification is backed by a publicly accessible URL, any IA can independently re-verify by fetching that URL and confirming the challenge token is present — without the user needing to re-authenticate. See [Evidence Sharing](/guides/evidence-sharing). --- ## Lokihub **Lokihub** is a self-custodial Lightning wallet and node interface for the [Flokicon](/ecosystem/flokicon) ecosystem. While the Zap Protocol handles how identities are resolved and how proof-of-payment is published, Lokihub is the infrastructure where your actual funds live. It lets anyone run their own Lightning node — in the cloud or at home — with a 1-click setup designed for Flokicoin. ## Core Capabilities By deploying a Lokihub instance, users control their own funds: ### 1. Self-Custodial Lightning Node Lokihub removes the technical friction of running a node. Once deployed, users have a dedicated, always-on Lightning node capable of managing channels, routing payments, and securing their balance non-custodially. ### 2. Native NWC Integration ([NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) ↗) Lokihub is built for native Nostr integration. Its **Nostr Wallet Connect** engine lets users instantly generate NWC pairing strings. This means you can connect your Lokihub node directly to `zapf.app` (or any other Nostr application) so that incoming tips flow instantly into your own infrastructure, completely bypassing custodial fallbacks. ### 3. App Ecosystem Support Beyond simple payments, Lokihub acts as a platform for your node. Users can install community extensions and applications to expand their node's capabilities—whether that means automating channel liquidity, setting up a point-of-sale system, or running automated routing scripts over the Flokicoin network. --- ### Set Up Your Own Node To receive micropayments directly without a custodian, deploy a Lokihub instance and pair it with Zapf. Get started by reading the **[Lokihub Documentation](https://docs.flokicoin.org/lokihub)** ↗. --- ## Supported Wallets Zapf is a non-custodial-first, open protocol powered by **Nostr Wallet Connect (NWC - [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) ↗)**. You can attach any compliant NWC wallet to your Zapf profile to sweep custodial funds or receive incoming zaps directly to your node. Not all wallets implement the full NWC spec. To successfully integrate with Zapf's Claiming Flow, a wallet must support the `make_invoice` and `pay_invoice` methods. ## Recommended NWC Wallets The following wallets have been tested and verified to work with Zapf's Zap Settlement Providers. | Wallet | Type | Setup Flow | Recommended Permission | |--------|------|------------|------------------------| | **Alby** | Browser Extension | Open Extension → Settings → NWC → Generate | `Make Invoice` | | **Mutiny** | Web App (PWA) | Settings → Wallet Connect → Add Connection | `Receive Only` | | **Zeus** | Mobile App / Node | Settings → Connect → NWC → Create New | `Make Invoice` | | **Strike** | Exchange App | Node Settings → NWC → Connect | `Receive Only` | ## Security Best Practices :::caution Zapf **does not** need permission to spend your funds. When generating an NWC connection string in your wallet application, you should always select the lowest necessary permission tier. ::: - **For Receiving Zaps:** Ensure the NWC string ONLY grants the `make_invoice` (or "Receive Only") permission. If a Zap Settlement Provider's database is ever compromised, the attacker will only be able to send *you* money, not drain your node. - **For Sending Zaps:** If you use Zapf as a client to *send* outgoing payments, you must grant the `pay_invoice` permission. You can mitigate risk by setting a strict daily spending limit (e.g., 50,000 loki/day) directly within your wallet's NWC settings. --- ## Adding a Legacy Identity Provider As a permissionless protocol, Zapf lets anyone integrate a new Legacy Identity Provider (LIDP). Whether you are adding a new social platform, messaging service, or custom authentication system, the process follows a standardized pattern. ## Understanding the Role When you add a LIDP, you are teaching a **Zap Settlement Provider (ZSP)** how to verify ownership of a platform account and how to construct the standardized Nostr events ([Identity Connection](/protocol/identity-connection-35521) and [IA Attestation](/protocol/ia-attestation-35522)) that prove it. ## The Integration Pattern Choose the verification method that fits the platform, then implement these shared steps: ### Step 1: Choose a Verification Method **Bot Challenge** (Discord, Telegram, and similar messaging platforms): - The ZSP runs a bot on the platform. - The user runs `/nostr-verify ` (Discord) or `/nostr_verify ` (Telegram) in a community channel; the bot responds with an `npv1` challenge token. - The user posts the challenge publicly in the same channel and submits the message link (`auth_type: "public_post"` — same model as other challenge-token providers). - Best for platforms where the ZSP can operate a verified bot identity. **Challenge Token** (X/Twitter, GitHub, Instagram, Facebook, Domain): - The ZSP issues a unique token tied to the user's pending session. - The user publishes that token on their account (tweet, gist, bio, DNS TXT record, etc.). - The ZSP fetches and verifies the published content via the platform's public API or HTTP. - Best for platforms with public content APIs. **OTP** (Email, Phone): - The ZSP sends a one-time code to the user's contact address or device. - The user submits the code to complete verification. - Best for private channels that don't support public proof publishing. ### Step 2: Identifier Normalization Before signing any attestations, the ZSP **must** normalize the identifier to prevent spoofing: - Email addresses: lowercase and strip aliases (e.g. `user+spam@gmail.com` → `user@gmail.com`). - Phone numbers: E.164 format (e.g. `+1234567890`). - Platform IDs: use the stable **numeric user ID** (not the username) as the raw identifier for ConnectionKey generation, since usernames can change on many platforms. Store the username separately as the display handle. - Domain: normalize to lowercase, strip trailing dots. ### Step 3: ConnectionKey Generation Generate the deterministic ConnectionKey for the Nostr events: ```go rawString := fmt.Sprintf("%s:%s", lidpName, normalizedIdentifier) connectionKey := sha256(rawString) ``` The `normalizedIdentifier` is the **stable numeric ID** (e.g. Discord snowflake, Telegram user ID) — not the username. This ensures the ConnectionKey stays the same even if the user changes their username. Store the username separately in the `Identity.Username` field so it can be embedded in zap receipt tags as a human-readable handle. ### Step 4: Constructing the Evidence Payload Build an `evidence` object recording the verification proof. All modes produce the same v1 format — always cleartext JSON, never encrypted: ```json { "version": 1, "lidp": "discord", "auth_type": "public_post", "user_id": "1254093577051574374", "username": "loki_nakamo", "verified_at": 1720000000, "evidence_url": "https://discord.com/channels/.../1506380827804831834", "challenge": "npv11qqsykd7ufyvfjl9qasdgtrz02jsv97l9atdnqc0vz8wsytxqxn9v6pqzvtph4", "pre_auth_code": "feb7dee63337" } ``` For OTP-based flows (e.g. email), `evidence_url`, `challenge`, and `pre_auth_code` will be absent since there is no public post. Cross-IA re-attestation is not possible for those connections. Serialize this to JSON and attach it as the `evidence` tag value on the [Kind 35522 Attestation](/protocol/ia-attestation-35522). The evidence is always cleartext — do **not** encrypt it. See [Evidence JSON Schema](/protocol/ia-attestation-35522#evidence-json-schema) for the full field reference. ### Step 5: Signing and Publishing Construct the [Attestation](/protocol/ia-attestation-35522): - Set the connection key. - Set the user's Nostr public key (if already registered). - Set the provider string (e.g. `"discord"`). - Set an appropriate `expiration`. Sign the event with the **IA's private key** and publish it to the designated Nostr relays. --- ## Claiming Funds If someone sends a zap to your Legacy Identity (e.g., your X handle, Telegram username, or GitHub account) *before* you have set up a Lightning wallet, the Zapf network does not reject the payment. Instead, the funds are safely escrowed via the [Fallback Lightning Address](/concepts/fallback-address) mechanism. This guide explains how to claim those funds through your Connections page. ## How to Claim ### 1. Log in Visit `zapf.app/login` and authenticate with your Nostr key. If you don't have one yet, the login flow will create one for you and guide you through connecting a wallet. ### 2. Go to your Connections page After logging in, navigate to your profile and open the **Connections** tab (`zapf.app/{your-id}/connections`). This page shows all Legacy Identity connections linked to your Nostr key — X, Telegram, GitHub, Instagram, email, and domain — along with any pending balances held in custodial fallback for each one. ### 3. Claim pending funds For any connection showing a pending balance, click **Claim**. The app will: 1. Confirm the connection belongs to you. 2. Use your connected wallet (NWC) to request a Lightning invoice. 3. Ask the ZSP to pay that invoice from its custodial ledger. Your balance clears in seconds. The connection's pending balance resets to zero once settlement completes. ### 4. Connect a wallet first (if needed) You need a Nostr Wallet Connect (NWC) string to receive funds. If you haven't set one up: 1. Open your Flokicoin wallet ([LokiHub](https://docs.flokicoin.org/lokihub)). 2. Go to **NWC** or **Nostr Wallet Connect** settings and generate a new connection pairing secret. 3. Paste the NWC string into your Zapf wallet settings. The NWC connection must have the ability to **receive invoices** (generate invoices on your behalf). ## The Underlying Protocol The claim process is a stateless, cryptographic sweep. When you click Claim, the Zapf web client: 1. Encrypts your NWC connection string using [NIP-44 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md) ↗, addressed exclusively to the ZSP's public key. 2. Submits the encrypted payload alongside your identity proof to the ZSP. The ZSP then: 1. Validates your identity proof against the provider's API. 2. Decrypts the payload using its private server key to read your NWC string. 3. Queries its internal ledger for all custodial [Zap Receipts](/protocol/zap-receipt-5521) attributed to the hash of your identifier (via the resolver tag). 4. Uses your NWC string to request a Lightning invoice from your wallet. 5. Pays that invoice instantly from its Master Node. Your dashboard updates to reflect a $0 pending balance, and the funds appear in your self-custodied wallet. --- ## Evidence Sharing Because the Zap Protocol is a federated open standard, users can strengthen their identity by having it verified by multiple independent Identity Authorities at once. ## How Cross-IA Verification Works All verification modes require the user to publish the challenge token publicly — in a Discord/Telegram message, a bio, tweet, gist, or DNS TXT record. The verification act is therefore independently re-checkable by anyone. The `evidence` tag in Kind 35522 contains the public URL of that post (`evidence_url`), the challenge that appeared in it (`challenge`), and the `pre_auth_code` used to generate it. Any IA can: 1. Read Kind 35522 from relays (via `#d` filter) 2. Fetch `evidence_url` 3. Confirm `challenge` is present in the content 4. **Cryptographically verify the challenge is session-bound**: bech32-decode the `npv1` token, strip the 2-byte TLV header to get the 32-byte session hash, then compute `SHA256(hex.Decode(p_tag_pubkey) || []byte(evidence.pre_auth_code))` and assert it matches. This proves the challenge was issued for this specific user and session — not replayed from another. 5. Issue its own Kind 35522 for the same `d` tag and `p` tag — **without requiring the user to re-verify** This is the zero-trust cross-IA attestation model: the public post is the proof, and anyone with relay access can verify it. ## Limitations - If the user deletes the post, cross-IA re-verification fails. The user must re-verify with each IA from scratch. - Bot Challenge evidence (Discord/Telegram) requires the post to still be publicly accessible at the `evidence_url`. Channel restrictions or post deletion will break re-verification. - A receiving IA may require a fresh verification regardless of existing evidence — this is a policy decision, not a protocol requirement. - **Privacy**: The `evidence` tag is always cleartext. `user_id` and `username` are visible to anyone who reads the relay. There is no privacy mode for evidence. --- ## Receiving Zaps The Zap Protocol makes it effortless to receive Lightning payments directly to your own wallet using **Nostr Wallet Connect (NWC)**. Because Zapf is non-custodial-first, the Zap Settlement Provider (ZSP) never holds your funds if you have properly configured a receiving wallet. ## What is NWC ([NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) ↗)? Nostr Wallet Connect is an open protocol that allows your Nostr client (or a service like Zapf) to securely communicate with a remote Lightning wallet. Instead of manually generating invoices, you provide Zapf with an encrypted NWC connection string. When someone attempts to zap your Discord or X account, the Zapf ZSP uses this NWC connection to quietly request an invoice from *your* wallet, hands that invoice to the sender, and the funds settle directly to your node. ## Setting Up Your Wallet To receive zaps non-custodially, you must connect a compatible NWC wallet to your Zapf profile. 1. **Log in to Zapf**: Go to the `zapf.app` dashboard and authenticate with your preferred Nostr extension (or generate a local key). 2. **Navigate to the Wallet Tab**: Find the "Wallet" or "Connections" section. 3. **Generate an NWC String**: Inside your lightning wallet app (e.g., Alby, Mutiny, Zeus, Strike), find the "NWC" or "Nostr Wallet Connect" settings. Generate a new connection pairing secret. - *Security Note*: When generating the NWC string in your wallet, configure it with **Receive Only** or **Make Invoice** permissions. Zapf does not need permission to spend your funds. 4. **Paste the String**: Enter the resulting `nostr+walletconnect://...` string into the Zapf dashboard. ## Custodial Mode (Fallback Lightning Address) If you have *not* connected an NWC wallet, or if your node happens to be offline when a zap arrives, the payment doesn't fail. Zapf automatically routes the payment to the [Fallback Lightning Address](/concepts/fallback-address). The ZSP will hold the funds in escrow, generate a custodial [Zap Receipt](/protocol/zap-receipt-5521), and notify you (e.g., via DM or Push Notification) that you have funds waiting to be claimed. To retrieve these funds, log in at `zapf.app/login` and go to your **Connections** page. Pending balances are shown per connection and can be claimed with one tap. See the [Claiming Funds](/guides/claiming-funds) guide for the full walkthrough. --- ## Running an Identity Authority (IA) Zapf is an open, federated protocol. While `zapf.app` operates a public Authority, anyone can spin up their own independent Identity Authority (IA). You might run a custom IA to support a specific community platform, or to offer a high-privacy attestation service free from central oversight. ## IA Server Requirements To operate a compliant IA on the Zapf network, your infrastructure needs: 1. **A Nostr Keypair**: A securely generated private key that will act as your Server Identity. You will use this to sign all [Attestations](/protocol/ia-attestation-35522). 2. **A Public Nostr Relay**: An accessible relay (e.g., `wss://relay.yourdomain.com`) where you will publish your attestations. This relay must have high uptime, as wallets will query it during the Deep Check verification phase. :::note A Lightning node, custodial ledger, and `.well-known/lnurlp/` server are required only if you are also operating as a **Zap Settlement Provider (ZSP)**. Pure IA operation does not require payment infrastructure. ::: ## Generating Keys and Evidence ### Connection Key The `d` tag in Kind 35522 is a deterministic **Connection Key** derived from the LIDP name and the user's normalized identifier: ``` ConnectionKey = hex( SHA256( lidp_name + ":" + normalized_lidp_id ) ) ``` Example: `SHA256("discord:1254093577051574374")` → the lowercase hex string used as the `d` tag. Normalization rules differ by LIDP (e.g., lowercase for email, numeric ID for Discord). Always normalize before hashing to ensure uniqueness. ### Challenge Token (`npv1`) The challenge token cryptographically binds a verification session to a specific user. See the full spec in [IA Attestation — The `npv1` Challenge Token](/protocol/ia-attestation-35522#the-npv1-challenge-token). Summary: 1. Generate a random `pre_auth_code` (e.g., 6-byte random hex string). 2. Compute `SHA256(hex.Decode(user_pubkey) || []byte(pre_auth_code))`. 3. Prepend the 2-byte TLV header `0x00 0x20`. 4. Bech32-encode the 34-byte result with the `npv1` human-readable part. The user must publish this token publicly (in a Discord message, tweet, DNS TXT record, etc.) at a URL you will record as `evidence_url`. ### Evidence Payload After the user publishes the challenge, construct the evidence JSON: ```json { "version": 1, "lidp": "discord", "auth_type": "public_post", "user_id": "1254093577051574374", "username": "joyosar", "verified_at": 1779219590, "evidence_url": "https://discord.com/channels/.../1506380827804831834", "challenge": "npv11qqsykd7ufyvfjl9qasdgtrz02jsv97l9atdnqc0vz8wsytxqxn9v6pqzvtph4", "pre_auth_code": "feb7dee63337" } ``` **`pre_auth_code` must always be included.** Cross-IA re-attestation requires it to verify the challenge is genuinely bound to this user and session. See [Cross-IA Challenge Binding](/protocol/ia-attestation-35522#cross-ia-challenge-binding). ## The Attestation Lifecycle As an IA operator, you are responsible for the entire lifecycle of the identity connections you certify. ### Signing and Confirming When a user authenticates with a LIDP via your service, you strictly normalize their identifier, generate the cryptographic ConnectionKey, and publish an **[Attestation](/protocol/ia-attestation-35522)** to your relay. The session moves to `confirmed` status. At this point, the Kind 35522 is on the relay but the LIDP→Nostr **routing link is not yet active**. The identity record is not written to the store until the user completes the activation step. ### User Activation (Required) After the Kind 35522 is published, your IA returns the raw attestation event to the user's client. The client extracts the event ID and your relay URL, builds a lightweight `e` tag reference, and publishes a **Kind 35521** event — without embedding the full attestation JSON. The client then sends an activation signal to your IA to confirm that Kind 35521 is live. Only after receiving this signal does your IA write the `Identity` record to its store — making LIDP→Nostr routing live. The session moves to `active` status. **If the user closes the dialog or declines without completing the activation step, no routing link is created.** :::note The activation signal mechanism is implementation-defined. Your IA must expose some interface for clients to trigger this step; the specific protocol (HTTP endpoint, Nostr event, etc.) and its path are entirely up to your implementation. ::: ### Dealing with Expiration Set a [NIP-40 expiration timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md) ↗ on every [Attestation](/protocol/ia-attestation-35522). The default is 90 days (configurable via `IA_ATTESTATION_EXPIRY_DAYS`). When an attestation expires, clients prompt the user to re-verify, ensuring identity ownership is periodically re-confirmed. ### Active Revocation If a user disconnects their identity from your dashboard, or if you detect abusive behavior, your IA must immediately publish a Kind 5 deletion event targeting the original [Attestation](/protocol/ia-attestation-35522). When wallets perform a Deep Check on your relay, they will see the deletion and reject out-of-date attestations. If the session was already `active` (user had signed and published Kind 35521), the IA must also delete the `Identity` routing record from its store to stop all zap routing to that link immediately. #### Summary of session states | Status | Kind 35522 on relay | Identity in store (routing live) | |--------|--------------------|---------------------------------| | `confirmed` | Yes | No | | `active` | Yes | Yes | | `revoked` | Deleted (Kind 5) | No | ## Establishing Trust Because Zapf is permissionless, simply running an IA does not mean the network will use it. Wallets and client applications (like the `zapf.app` web client) maintain a list of *Trusted IA Pubkeys*. To get your IA recognized broadly across the ecosystem, you must build a reputation for securely verifying identities, protecting user data, and maintaining stable infrastructure. --- ## Sending Zaps As an open protocol, Zapf extends standard [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ and LUD-16 payment flows with a permissionless identity resolution step. This lets your wallet or app route funds to user-friendly identifiers like `discord:username#1234` or `email:user@example.com`. ## The Sender Flow As an integrator (wallet developer or app builder), implementing the Zapf sending flow involves 4 steps: ### 1. Resolve the Identity When a user enters a legacy identifier into your UI, your client needs to discover the corresponding LNURL endpoint. By default, the `zapf.app` Zap Settlement Provider supports resolving any supported LIDP string natively: ```text # Format: @ # Where connection_key is the SHA256 hash of: SHA256(lidp_name:stable_id) e.g. SHA256(discord:1254093577051574374) a1b2c3d4e5f6g7h8...@zapf.app ``` ### 2. Fetch the LNURL Metadata (LUD-16) Perform a standard `GET` request to the `.well-known` endpoint of the Zap Settlement Provider domain using the derived connection key: ```bash curl https://zapf.app/.well-known/lnurlp/a1b2c3d4e5f6g7h8... ``` The ZSP will return a standard LNURL payload. Key fields include: - `callback`: The URL to request the final Lightning invoice. - `minSendable` / `maxSendable`: The payment boundaries (in millisats). - `allowsNostr`: `true` (indicating you should generate a Zap Request). - `nostrPubkey`: The ZSP's pubkey (for verifying the eventual receipt). ### 3. Generate a Zap Request ([Kind 5520](/protocol/zap-request-5520)) Instead of a standard [Kind 9734](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ Zap Request, generate a [Kind 5520](/protocol/zap-request-5520) event. Sign the event with the sender's Nostr private key. The `p` tag must contain the recipient's target (either their Nostr pubkey if known, or the SHA-256 hash of the `lidp:identifier` string). If this is a proxy zap from a bot, you will use [Kind 5523](/protocol/on-behalf-zap-request-5523). URL-encode the JSON string representation of this signed event. ### 4. Fetch the Invoice and Pay Make a request to the `callback` URL you received in Step 2, appending the `amount` (in millisats) and the URL-encoded `nostr` parameter containing your [Kind 5520](/protocol/zap-request-5520): ```bash curl "https://zapf.app/api/lnurl/cb/a1b2c3d4e5f6g7h8...?amount=1000000&nostr=%7B%22kind%22%3A5520..." ``` The ZSP will respond with a `pr` containing a BOLT11 Lightning Invoice. Your client can now pay this invoice via its connected Lightning node or wallet. ## The Receipt Once the payment settles, the ZSP will automatically publish a [Zap Receipt](/protocol/zap-receipt-5521) to the relays specified in your original [Kind 5520](/protocol/zap-request-5520) or [Kind 5523](/protocol/on-behalf-zap-request-5523) request. Your client can listen on those relays to instantly update the UI with a "Payment Confirmed" state, verifying the receipt signature against the [`nostrPubkey`](/protocol/lnurl#lud-16-internet-identifiers) provided in Step 2. --- ## What is Zapf? Zapf is an application and Zap Settlement Provider (ZSP) that bridges the **Nostr** protocol and **Lightning** network to web identities by implementing the Zap Protocol. Our mission is simple: **Zap Anyone, Anywhere.** With Zapf, you can send micro-payments (zaps) to anyone using identifiers they already have—like an Email address, Discord ID, X handle, or Phone number—even if they haven't set up a wallet yet. ## The Core Problem Traditional Lightning or crypto payments require the recipient to generate an invoice. This means both sender and recipient must be online, and the recipient must already have a wallet. This creates friction when onboarding new users to energy-backed coins like Bitcoin and Flokicoin. Zapf solves this by elevating Nostr's [**NIP-57 (Zaps)**](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ standard beyond native pubkeys. It lets you send payments across chains and platforms without locking into one provider. Zapf follows the [Web of Fun (WoF)](https://docs.flokicoin.org/wof/) ↗ philosophy: a cooperative, permissionless web where identity and payments work together. ## The Zap Protocol Implementation At its core, the Zap Protocol relies on a strict separation of concerns—think of it like a credit card transaction: - **Identity Authority (IA)**: Acts as the "Bank". It verifies who you are (e.g., via challenge-token — bot-assisted for Discord/Telegram) and cryptographically links that identity to a Nostr public key. - **Zap Settlement Provider (ZSP)**: Acts as the "Payment Processor". It manages the Lightning/Flokicoin node, moves the money, and issues the final cryptographic receipt. Using this architecture, Zapf serves as a multi-chain gateway built on Nostr that connects traditional accounts to crypto wallets non-custodially: 1. **Universal Identity**: Map any abstract legacy identifier (`email:loki@gmail.com`) to a Nostr public key via an [Identity Connection](/protocol/identity-connection-35521). 2. **Fallback Escrow**: If the user doesn't have a wallet, a [settlement server](/concepts/zap-settlement-provider) holds the funds securely via the [Fallback Address](/concepts/fallback-address) and notifies them. 3. **Stateless Claiming**: Users prove ownership of their legacy identifier, generating a local Nostr keypair to publish their connection, which signals the server to sweep the funds to their new node. 4. **Verifiable Settlement**: Every payment generates a cryptographically verifiable [Zap Receipt](/protocol/zap-receipt-5521), ensuring finality even for custodial payments. ## Key Features - **User-Owned Profiles**: Users own their social graph and payment routing preferences via [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) ↗. - **NWC Native**: Full support for Nostr Wallet Connect ([NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) ↗) for 1-click, permissionless sending and receiving. - **Multi-Chain Zaps**: Native support for standard [NIP-57 zaps](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ and [On-Behalf Proxy Zaps](/protocol/on-behalf-zap-request-5523) open to all energy-backed liquidity networks (like Bitcoin and Flokicoin). - **Protocol Extensibility**: Because of the separation of concerns, anyone can run their own [Identity Authority (IA)](/concepts/identity-authority), operate an independent [settlement server (ZSP)](/concepts/zap-settlement-provider), or integrate new [identity providers](/concepts/legacy-identity-providers). ## Where to Next? - Understand the architecture in [**Core Concepts**](/category/concepts) - Integrate the Zap Protocol via [**Protocol Specs**](/protocol/zap-request-5520) - Learn how to interact with the network in [**Guides**](/category/guides) - Explore the tools in [**Ecosystem & LIDPs**](/ecosystem/flokicon) --- ## IA Attestation # IA Attestation (Kind 35522) As part of Zapf's open identity model, a **Kind 35522** (IA Attestation) is the cryptographic proof generated by an Identity Authority (IA) stating: *"I have verified that Nostr Public Key X owns LIDP Account Y."* This event is generated by the IA server immediately upon completing a successful verification flow and is published to the IA's own relays. ## Event Structure ```json { "kind": 35522, "content": "", "tags": [ ["d", ""], ["p", ""], ["lidp", ""], ["evidence", ""], ["expiration", ""] ], "pubkey": "", "created_at": "", "id": "...", "sig": "..." } ``` ## Required Tags | Tag | Format | Description | |-----|--------|-------------| | `d` | `["d", ""]` | Deterministic connection key: `SHA256(lidp_name:lidp_id)`. | | `p` | `["p", ""]` | The target Nostr public key that proved ownership. | | `lidp` | `["lidp", ""]` | The name of the Legacy Identity Provider (e.g., `discord`, `email`). | | `evidence` | `["evidence", ""]` | Verification payload containing proof of identity (see below). | | `expiration` | `["expiration", ""]` | [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) ↗ expiration timestamp. Default 90 days from issuance (`IA_ATTESTATION_EXPIRY_DAYS`). Set `ExpirationDays: 0` to omit expiration. | ## The `evidence` Tag The `evidence` tag contains proof that the IA used to verify the user's LIDP account. The value is always a cleartext JSON string (v1 format). `auth_type` is always `"public_post"` — all supported verification modes require the user to publish the challenge token publicly. | Field | Type | Description | |-------|------|-------------| | `version` | `number` | Always `1`. | | `lidp` | `string` | The LIDP name (e.g., `"discord"`). | | `auth_type` | `string` | Always `"public_post"`. | | `user_id` | `string` | The normalized LIDP account identifier (e.g., Discord user ID). | | `username` | `string` | The provider handle at verification time (e.g., `"joyosar"`). | | `verified_at` | `number` | Unix timestamp when the verification was completed. | | `evidence_url` | `string` | URL of the public post or profile where the challenge appeared. | | `challenge` | `string` | The `npv1…` challenge token that was published at `evidence_url`. | | `pre_auth_code` | `string` | The raw pre-auth code used to generate `challenge`. Required for [cross-IA cryptographic verification](#cross-ia-challenge-binding). | The `evidence` tag enables [Evidence Sharing](/guides/evidence-sharing): since the verification is backed by a publicly accessible URL, any IA can independently re-verify the same identity by fetching `evidence_url` and confirming the `challenge` is present. ## Evidence JSON Schema A complete v1 evidence payload looks like this: ```json { "version": 1, "lidp": "discord", "auth_type": "public_post", "user_id": "1254093577051574374", "username": "joyosar", "verified_at": 1779219590, "evidence_url": "https://discord.com/channels/.../1506380827804831834", "challenge": "npv11qqsykd7ufyvfjl9qasdgtrz02jsv97l9atdnqc0vz8wsytxqxn9v6pqzvtph4", "pre_auth_code": "feb7dee63337" } ``` This JSON is stored as-is in the `evidence` tag value. ## The `npv1` Challenge Token The `challenge` field contains a **bech32-encoded TLV** token with the `npv1` prefix. It cryptographically binds the challenge to a specific user+session, preventing replay attacks across sessions. ### TLV Structure ``` type=0x00 || length=0x20 || value=SHA256(nostrPubkeyBytes || preAuthCodeBytes) ``` - **Type** `0x00`: session hash (32 bytes). - **Length** `0x20`: always 32 (hex `20`). - **Value**: `SHA256(hex.Decode(p_tag_pubkey) || []byte(pre_auth_code))`. The resulting 34-byte TLV is bech32-encoded with the `npv1` human-readable part. ### Cross-IA Challenge Binding {#cross-ia-challenge-binding} When a second IA wants to re-attest an identity from existing evidence, it must verify the challenge is genuinely bound to the claimed user and session — not replayed from a different session. The algorithm: 1. Bech32-decode the `challenge` token to get the 34-byte TLV payload. 2. Strip the 2-byte TLV header (`0x00 0x20`) → 32-byte session hash. 3. Compute `SHA256(hex.Decode(p_tag_pubkey) || []byte(evidence.pre_auth_code))`. 4. Assert both hashes match. Step 4 is what separates a genuine session-bound challenge from a replayed token. Without `pre_auth_code` in the evidence, step 3 is impossible — this is why `pre_auth_code` must always be included in the evidence payload. ## Event Lifecycle 1. **Creation**: User verifies identity with the IA. IA signs Kind 35522. 2. **Publishing**: IA publishes the event to its designated relays. The session moves to `confirmed` status. **The LIDP→Nostr routing link is NOT yet active at this point.** 3. **Linking**: The IA returns the raw Kind 35522 event to the user's client. The client extracts the event ID and IA relay URL, builds an `e` tag referencing the attestation, and publishes a [Kind 35521](/protocol/identity-connection-35521) event. The full Kind 35522 is NOT embedded — consumers fetch it from the IA relay on demand. 4. **Activation**: After publishing Kind 35521, the client notifies the IA that the connection event is live. The IA writes the `Identity` record to its store, making the LIDP→Nostr routing link live. The session moves to `active` status. **Routing is only live after this step.** The mechanism for this notification is IA-implementation-defined; the protocol only requires that routing is not activated until the IA receives this signal. 5. **Revocation**: If a user disconnects their account, the IA publishes a Kind 5 (Event Deletion) targeting the Kind 35522 ID. This instantly invalidates the attestation during a Deep Check. If the session was `active`, the IA also removes the `Identity` routing record from its store. --- ## Identity Connection # Identity Connection (Kind 35521) A **Kind 35521** is a publicly verifiable Nostr event published by an end-user to announce that their Nostr public key is linked to a Legacy Identity Provider (LIDP) account. Crucially, because an end-user *could* lie about owning an account, Kind 35521 alone is insufficient. It must reference cryptographic proof signed by a trusted third party—an [Identity Authority (IA)](/protocol/ia-attestation-35522). ## Event Structure ```json { "kind": 35521, "pubkey": "", "created_at": "", "tags": [ ["d", ""], ["e", "", ""], ["lidp", ""] ], "content": "", "id": "...", "sig": "..." } ``` ## Required Tags | Tag | Format | Description | |-----|--------|-------------| | `d` | `["d", ""]` | Deterministic connection key: `SHA256(lidp_name:lidp_id)` (e.g., `a1b2c3...`). Note: Including a platform prefix here is strictly invalid. | | `lidp` | `["lidp", ""]` | The name of the Legacy Identity Provider (e.g., `discord`, `x`) for quick relay filtering. | | `e` | `["e", "", ""]` | A reference to the **[Kind 35522](/protocol/ia-attestation-35522)** attestation event signed by the IA. Clients must fetch the full event from the IA relay to verify. See below. | ## The `nconnection` String Format To make identity links portable and easy for users to share between clients (e.g., from a Discord bot to a mobile wallet), Zapf uses a custom bech32-encoded string format with the prefix `nconnection`. An `nconnection` string bundles the deterministic `connection_key` with the relays where the user's Kind 35521 events are published. ### Encoding Structure (TLV) The format follows a Type-Length-Value (TLV) structure similar to [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) identifiers: | Type | Name | Length | Description | |------|------|--------|-------------| | `0` | `Special` | 32 bytes | The raw bytes of the `connection_key` (hex decoded). | | `1` | `Relay` | Variable | A UTF-8 string of a Nostr relay URL. Can be repeated. | **Example:** `nconnection1qqpx9er9wehxum59ahx7u3z9yhxw7msv9ujumn9wskz6un9d3shjtnyv9khq6t9wshx67m9vsuqgyp96mzk6uayz9p8m94qqqqqqqf4k9mx` ## The `e` Tag and Multi-IA Stacking The `e` tag references the [Kind 35522](/protocol/ia-attestation-35522) attestation published by the IA to its relay. - Index 1: The event ID of the IA's [Kind 35522](/protocol/ia-attestation-35522) attestation. - Index 2: A relay hint where clients must fetch the full [Kind 35522](/protocol/ia-attestation-35522) to verify. The attestation is **not embedded** in the Kind 35521 event. Clients fetch it from the IA relay on demand. This keeps the event lightweight (~100 bytes per attestation reference vs. ~1–2 KB for embedded JSON). **Stacking**: A user can include *multiple* `e` tags in their Kind 35521 event. If a user proves their Discord account to three different IAs, they can reference all three attestations. A client only needs to trust *one* of those IAs to consider the connection verified. ## Content Field The `content` field contains the user's **profile metadata** as cleartext JSON. It includes display information passed down from the IA, as well as key identity fields so clients can display the connection without fetching the attestation from the relay: ```json { "display_name": "loki_nakamo", "picture": "https://cdn.discord.com/...", "user_id": "123456789", "username": "loki_nakamo" } ``` | Field | Source | Description | |-------|--------|-------------| | `display_name` | IA profile data | Human-readable display name on the LIDP | | `picture` | IA profile data | Avatar URL | | `user_id` | IA evidence | Platform-specific numeric or string user ID | | `username` | IA evidence | Platform handle / username | All content fields are globally readable on the Nostr network. The authoritative values for `user_id` and `username` are in the Kind 35522 `evidence` tag — the content fields here are a convenience copy for quick display. ## Security & Spoofing Because Kind 35521 is a user-signed event, a user could theoretically place malicious or spoofed data into the `content` field (e.g., claiming their display name is "Elon Musk" when it is not). **This does not affect the protocol:** 1. **Routing ignores content**: The ZSP strictly routes payments based on the cryptographic hash in the `d` tag. A user cannot spoof the payment destination by changing their event content. 2. **Verification catches spoofing**: The `"e"` tag references the unforgeable [Kind 35522](/protocol/ia-attestation-35522) attestation signed by the IA. Clients must cross-reference the `content` claims against the IA's verified payload fetched from the relay. If they do not match, the client UI should flag the connection as spoofed. --- ## LNURL Integration The **Zap Protocol** implements the open **LUD-06** and **LUD-16** standards to bridge Lightning Network wallets with Nostr identity resolution. ## LUD-16: Internet Identifiers LUD-16 allows Lightning wallets to resolve human-readable identifiers formatted like email addresses (e.g., `user@example.com`). Any compliant service can expose its identity graph to standard Lightning wallets through this mechanism. Because Zap Settlement Providers (ZSPs) generate on-the-fly Lightning invoices for legacy accounts on behalf of users, standard wallets (like Phoenix or Wallet of Satoshi) can unknowingly pay a Discord user by resolving a Lightning Address. ### Identifier Resolution The `username` portion of the LUD-16 address can be one of three things, resolved in priority order: | Priority | Type | Example | How it resolves | |----------|------|---------|----------------| | 1 | **[NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) ↗ handle** | `alice@example.com` | Looked up in the identity store by name | | 2 | **ConnectionKey hash** | `a1b2c3...@example.com` (64-char hex) | Looked up in the connection store by hash | | 3 | **Raw pubkey** | `@example.com` (64-char hex) | Used directly as the recipient pubkey | For social LIDP connections (Discord, GitHub, etc.), the identifier is the **connectionKey hash** — `SHA256(lidp_name:lidp_id)`. Clients never expose the raw LIDP username in the LNURL path. ```http GET https://example.com/.well-known/lnurlp/ ``` ### The Callback Response The service resolves the identifier via the priority table above, then responds with a standard LNURL payload: ```json { "callback": "https://example.com/lnurl/callback/", "maxSendable": 100000000, "minSendable": 1000, "metadata": "[[\"text/plain\",\"Zap @example.com\"],[\"text/identifier\",\"@example.com\"],[\"chain/flokicoin\",\"loki\"]]", "tag": "payRequest", "commentAllowed": 205, "allowsNostr": true, "nostrPubkey": "" } ``` ### Nostr Extensions - `allowsNostr: true` signals to [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ capable clients that they can submit a [Kind 5520](/protocol/zap-request-5520) or [Kind 5523](/protocol/on-behalf-zap-request-5523) Zap Request to the callback URL. - The `nostrPubkey` is the pubkey that will sign the resulting [Kind 5521](/protocol/zap-receipt-5521) Zap Receipt. This is typically the recipient's pubkey, or the server's pubkey for unclaimed/anonymous users. ## Invoice Generation (Callback) When the client accesses the `callback` URL (e.g., `GET https://example.com/lnurl/callback/...&amount=10000&nostr=`), the server executes the following logic: 1. **Validation**: Validate the [Kind 5520](/protocol/zap-request-5520) or [Kind 5523](/protocol/on-behalf-zap-request-5523) against the requested amount. 2. **Target Resolution**: Is the target a registered user with a connected NWC wallet? - **Yes (Direct)**: The server acts as a passthrough, retrieving an invoice directly from the user's NWC node. - **No (Custodial)**: The server generates an invoice from its own Master Node (the Fallback Lightning Address flow). 3. **Invoice Response**: The server returns the generated `pr` (payment request) to the client. ```json { "pr": "lnfc10n1...", "routes": [] } ``` ## Multi-Chain Support The standard LUD-06/LUD-16 `metadata` array is extended with **chain tags** to signal which Lightning Network chains and denominations the service accepts. This is a backward-compatible extension — existing Bitcoin-only wallets simply ignore unknown metadata entries and continue to function normally. ### Chain Metadata Tags Chain support is declared via optional entries in the `metadata` JSON array using the format `chain/`: | Tag | Denomination | Description | |-----|-------------|-------------| | `chain/flokicoin` | `loki` | Flokicoin Lightning Network | If **no** chain tags are present, clients default to `chain/flokicoin`. If at least one chain tag is present, clients should assume only the listed chains are supported. ### Example: Flokicoin Service A service operating on the Flokicoin Lightning Network includes the `chain/flokicoin` metadata tag with the `loki` denomination: ```json ["chain/flokicoin", "loki"] ``` Amounts in `minSendable` and `maxSendable` are denominated in the **smallest sub-unit** of the declared chain, and the returned `pr` invoice conforms to the respective chain's Lightning invoice format. | Chain | Unit Name | Example | |-------|-----------|---------| | `chain/flokicoin` | milliloki (mloki) | `1000` = 1 loki | | `chain/bitcoin` | millisatoshi (msats)| `1000` = 1 sat | ### Flokicoin Wallet Compatibility Standard Lightning wallets that do not understand `chain/flokicoin` will silently ignore the tag and attempt to pay the returned invoice. This will fail at the protocol level because the invoice belongs to a different chain — no funds are lost, no cross-chain confusion is possible. The wallet simply rejects the invoice as unpayable. When a service supports multiple chains, it includes multiple chain tags, and the callback negotiates the correct chain based on the client's preference. --- ## On-Behalf Zap Request # On-Behalf Zap Request (Kind 5523) As an extension to the open protocol ecosystem, **Kind 5523** (On-Behalf Zap Request) is a specialized variant of the standard [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ Zap Request ([Kind 5520](/protocol/zap-request-5520)). It was introduced by the Zap Protocol to securely support "proxy" or "custodial" zaps made on behalf of external, off-chain users by Proxy Agents (e.g., bots). Standard [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ assumes that the `pubkey` signing the Zap Request is the literal economic sender of the funds. When a custodial bot—such as a proxy bot application—generates a Zap Request, standard Nostr clients can incorrectly attribute the funds or social signal to the bot itself rather than the true user. **Kind 5523** solves this semantic ambiguity by enforcing that aware clients must ignore the event `pubkey` and strictly inspect the `p` (Recipient) and `P` (Sender) tags to ascertain the correct connection identities. ## Structure The event generally maps 1:1 with the standard [Kind 5520](/protocol/zap-request-5520) structure, except for its specific kind identifier (`5523`) and its rigid expectation that the attribution tags will use 3 elements instead of 2. ```json { "kind": 5523, "content": "Sent via Proxy Application", "tags": [ ["p", "", ""], ["P", "", ""], ["amount", "100000"], ["chain", "flokicoin"], ["relays", "wss://relay.zapf.app", "wss://relay.damus.io"], ["lnurl", "lnurl..."] ], "pubkey": "", "created_at": 1709424000, "sig": "..." } ``` ### Tag Explanations - **`p` (Recipient):** Instead of a raw Nostr pubkey, this tag expects `[ "p", "", "" ]`. - **`P` (Sender):** Instead of a raw Nostr pubkey, this tag expects `[ "P", "", "" ]`. - **`amount`:** Requested strictly in `mloki` or `msats` (milli-units). - **`chain`:** Required context. Must be `flokicoin` or `bitcoin`. ## Verification & Parsing 1. **ZSP Integration:** The Zap Settlement Provider (ZSP) handles `5523` alongside `5520` via its LNURL-Pay webhook callback, ensuring identical amount verification and wrapping the 5523 string inside the resulting `5521` (Zap Receipt) `description` tag. 2. **Client UI Resolution:** The Nostr client parses the `description` tag. Upon detecting a `5523` event, it invokes a network query for [Kind 35521](/protocol/identity-connection-35521) using the `#d` filter (``). This permits the UI to translate the external ConnectionKey back into a native Nostr `Kind 0` profile metadata object dynamically. If the identity connection query returns nothing, the UI displays the Fallback LIDP profile data (e.g., Provider Avatar) returned by the ZSP implementation's API. --- ## Zap Receipt # Zap Receipt (Kind 5521) A **Kind 5521** (Zap Receipt) is the open standard published by a Lightning Network provider *after* a Lightning invoice has been successfully paid. It serves as the definitive, cryptographic proof on the Nostr network that funds were settled against a specific Zap Request. ## Event Structure The receipt is signed by the **provider's** key (the node that received the funds) and is published to the relays specified in the original Zap Request. ```json { "kind": 5521, "content": "", "tags": [ ["e", ""], ["a", ""], ["p", "", "", ""], ["P", "", "", ""], ["r", ""], ["R", ""], ["amount", ""], ["chain", "flokicoin"], ["bolt11", "lnfc10u1..."], ["description", ""], ["preimage", "<32_byte_hex_string>"] ], "pubkey": "", "created_at": "", "id": "...", "sig": "..." } ``` ## Required Tags | Tag | Format | Description | |-----|--------|-------------| | `e` | `["e", ""]` | Used if the Zap is in response to a specific Nostr event (copied from request). | | `a` | `["a", "::"]` | Used if the Zap is in response to a parameterized replaceable event (copied from request). | | `p` | `["p", "", "", ""]` | The recipient. The optional 4th element is the stable provider handle (e.g. Discord username, Telegram @username). May be appended post-settlement for Direct Payments. | | `P` | `["P", "", "", ""]` | The sender. The optional 4th element is the stable provider handle. This structure supports proxy/anonymous routing. | | `amount` | `["amount", ""]` | The payment amount extracted from the BOLT11 invoice. | | `chain` | `["chain", ""]` | The settled network. | | `bolt11` | `["bolt11", ""]` | The exact Lightning invoice that was paid. | | `description` | `["description", ""]` | **Required.** The raw JSON string of the original [Kind 5520](/protocol/zap-request-5520) or [Kind 5523](/protocol/on-behalf-zap-request-5523) Zap Request. | | `preimage` | `["preimage", ""]` | The cryptographic proof of payment (32-byte hex string) proving the invoice was settled. | ## Optional Tags | Tag | Format | Description | |-----|--------|-------------| | `r` | `["r", ""]` | The true, verified Nostr pubkey of the recipient, provided by the ZSP if they resolved the `p` tag ConnectionKey. | | `R` | `["R", ""]` | The true, verified Nostr pubkey of the sender, provided by the ZSP if they resolved the `P` tag ConnectionKey. Enables "Social Bridge" UX. | ## Identity Tag Matrix When generating or parsing a 5521 Zap Receipt, use this matrix to correctly resolve identity tags based on the parent intent kind: | Intent Kind | Sender (`P`) | Recipient (`p`) | Resolved Recipient (`r`) | Resolved Sender (`R`) | | --- | --- | --- | --- | --- | | **5520** | **Pubkey** | Pubkey OR ConnKey | If `p` is ConnKey | N/A (Already Pubkey) | | **5523** | **ConnKey** | **ConnKey** | If resolved | **If resolved** | ## Standard vs. Proxy Zaps This single Kind 5521 event covers multiple types of transactions: 1. **Standard Zaps**: Generated in response to a [Kind 5520](/protocol/zap-request-5520) Request. The `description` tag contains the stringified intent. 2. **Proxy Zaps**: Generated in response to a [Kind 5523](/protocol/on-behalf-zap-request-5523) On-Behalf Request. The `description` tag contains the proxy intent. ## Custodial Receipts When a payment is made via the [Fallback Lightning Address](/concepts/fallback-address) model, the service holds funds temporarily on behalf of the recipient. This applies in two scenarios: - **LIDP recipients**: The target has no Nostr keypair yet. The `p` tag contains their **ConnectionKey**. - **Nostr recipients**: The target has a pubkey but their wallet is unreachable. The `p` tag contains their **Nostr pubkey**. In both cases, the receipt is stored internally by the server as a pending custodial balance. When the recipient later authenticates and provides a responsive wallet, the server sweeps the funds. --- ## Zap Request # Zap Request (Kind 5520) To initiate a payment across the open network, a client generates a **Kind 5520** (Zap Request) event. This extends the standard [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗ Zap Request ([Kind 9734](https://github.com/nostr-protocol/nips/blob/master/57.md) ↗) with two key changes that motivated new event kinds: 1. The `p` tag allows targeting legacy identities by accepting a **ConnectionKey** instead of strictly requiring a Nostr pubkey. The 3rd parameter denotes the provider (e.g., `discord`). If the 3rd parameter is empty or omitted, it defaults safely back to a native `nostr` pubkey. 2. Settlement is coin-agnostic, supporting any energy-backed asset routable over the Lightning Network — not limited to Bitcoin. ## Event Structure A Zap Request is created and signed by the **sender's** client. ```json { "kind": 5520, "content": "Here is 1000 loki! Great post.", "tags": [ ["e", ""], ["a", ""], ["k", ""], ["p", "", ""], ["amount", "1000000"], ["chain", "flokicoin"], ["lnurl", "lnurl1..."], ["relays", "wss://relay.ohstr.com", "wss://relay.primal.net"] ], "pubkey": "", "created_at": "", "id": "...", "sig": "..." } ``` ## Required Tags | Tag | Format | Description | |-----|--------|-------------| | `p` | `["p", "", ""]` | The target of the Zap. Can be a native Nostr 64-character hex key or a ConnectionKey. The 3rd element denotes the provider (e.g., `discord`). If omitted or empty, it defaults to `nostr` (pubkey). | | `relays` | `["relays", "", ""]` | An array of relay URLs where the provider should publish the resulting Zap Receipt. | | `amount` | `["amount", ""]` | The requested payment amount in milli-units (e.g., `mloki`, `msats`). | | `chain` | `["chain", ""]` | The network context, e.g., `flokicoin` or `bitcoin`. | | `lnurl` | `["lnurl", ""]` | The encoded LNURL the sender is requesting an invoice from. | ## Optional Tags | Tag | Format | Description | |-----|--------|-------------| | `e` | `["e", ""]` | Used if the Zap is in response to a specific Nostr event (e.g., tipping a specific note). | | `a` | `["a", "::"]` | Used if the Zap is in response to a parameterized replaceable event (e.g., tipping an article or badge). | | `k` | `["k", ""]` | The stringified kind of the target event being zapped (e.g., `"1"`, `"30023"`). | ## Validation Rules When a service or LN provider receives a [Kind 5520](/protocol/zap-request-5520) via an LNURL callback, it must strictly validate: 1. **Signature**: The event signature must be mathematically valid for the `pubkey`. 2. **Amount**: The `amount` tag must match the `amount` query parameter sent in the HTTP request. 3. **LNURL**: The `lnurl` tag must match the target being requested. 4. **Chain Gating**: The requested `chain` tag must match the provider's allowed routing chains. If these conditions are met, the provider generates a Lightning invoice with the hash of this encoded JSON event embedded in the invoice's description hash (per LUD-11).