A lightning zap sends sats from the user’s Lightning wallet to a recipient identified by either a DID (Decentralized Identifier), an alias, or a LUD-16 Lightning Address (e.g. user@domain.com).
The flow has three distinct phases:
The user invokes lightning-zap <recipient> <amount> [memo] from the CLI. The Keymaster class inspects the recipient string:
@ and does not start with did:, it is treated as a LUD-16 Lightning Address and used directly.Keymaster also loads the sender’s wallet to retrieve the sender’s Lightning configuration, including the wallet credentials needed by the Lightning mediator to create invoices, pay invoices, and query payment status.
Keymaster delegates to Drawbridge, which proxies Lightning work to the Lightning mediator. The mediator handles both recipient types:
LUD-16 Lightning Address
The Lightning mediator parses the user@domain string and performs two HTTP requests against the recipient’s LNURL server (SSRF-protected — HTTPS required, private IPs blocked):
GET https://domain/.well-known/lnurlp/user — retrieves the LNURL pay metadata, including the callback URL and the min/max sendable amounts.GET {callback}?amount={msats}&comment={memo} — requests a BOLT11 invoice for the specific amount (converted to millisats). The recipient’s LNURL server asks their Lightning node to generate the invoice and returns it in the response.DID-based Zap
The Lightning mediator resolves the recipient DID via Gatekeeper to obtain their DID Document, then locates the #lightning service endpoint. It validates the endpoint URL (.onion addresses must use http:// and are proxied via Tor; clearnet addresses must use https://). It then calls GET {serviceEndpoint}?amount={sats}&memo={memo}, and the recipient’s Lightning service generates and returns a BOLT11 invoice.
The Lightning mediator submits the BOLT11 invoice to the sender’s configured Lightning backend, which routes the payment across the Lightning Network to the recipient’s node. In the bundled stack that backend is LNbits (POST /api/v1/payments), but the mediator owns that integration boundary. Once the recipient’s node settles the payment and returns the preimage, the backend confirms success to the mediator. The mediator returns the paymentHash up the call stack through Drawbridge to the CLI, which prints it as JSON.
sequenceDiagram
actor User
participant CLI
participant Keymaster
participant DrawbridgeClient as Drawbridge Client<br/>(KeymasterClient)
participant DrawbridgeAPI as Drawbridge API<br/>(POST /lightning/zap)
participant LightningMediator as Lightning Mediator<br/>(/api/v1/lightning/zap)
participant Gatekeeper
participant RecipientServer as Recipient<br/>LNURL / DID Service
participant RecipientNode as Recipient<br/>Lightning Node
participant SenderBackend as Sender<br/>Lightning Backend
participant LN as Lightning Network
User->>CLI: lightning-zap <recipient> <amount> [memo]
CLI->>Keymaster: zapLightning(recipient, amount, memo)
alt recipient is LUD-16 (user@domain)
Note over Keymaster: isLud16 = true, use as-is
else recipient is DID or alias
Keymaster->>Gatekeeper: lookupDID(id)
Gatekeeper-->>Keymaster: did
end
Keymaster->>Keymaster: getLightningConfig()<br/>load wallet → fetch Lightning wallet config
Keymaster->>DrawbridgeClient: drawbridge.zapLightning(config, recipient, amount, memo)
DrawbridgeClient->>DrawbridgeAPI: POST /lightning/zap<br/>{did, amount, memo}
DrawbridgeAPI->>LightningMediator: POST /api/v1/lightning/zap
alt LUD-16 Lightning Address (user@domain)
LightningMediator->>LightningMediator: Parse user@domain<br/>Validate domain (SSRF check)
LightningMediator->>RecipientServer: GET https://domain/.well-known/lnurlp/user
RecipientServer-->>LightningMediator: {callback, minSendable, maxSendable, ...}
LightningMediator->>LightningMediator: Validate callback URL (HTTPS, SSRF check)<br/>Convert sats → msats, check limits
LightningMediator->>RecipientServer: GET {callback}?amount={msats}&comment={memo}
RecipientServer->>RecipientNode: Create invoice for {msats}
RecipientNode-->>RecipientServer: BOLT11 invoice
RecipientServer-->>LightningMediator: {pr: <BOLT11 invoice>}
else DID-based Zap
LightningMediator->>Gatekeeper: resolveDID(did)
Gatekeeper-->>LightningMediator: DID Document
LightningMediator->>LightningMediator: Find #lightning service endpoint<br/>Validate URL (onion→http, clearnet→https, SSRF check)
LightningMediator->>RecipientServer: GET {serviceEndpoint}?amount={sats}&memo={memo}<br/>(via Tor proxy if .onion)
RecipientServer->>RecipientNode: Create invoice for {sats}
RecipientNode-->>RecipientServer: BOLT11 invoice
RecipientServer-->>LightningMediator: {paymentRequest: <BOLT11 invoice>}
end
LightningMediator->>SenderBackend: payInvoice(bolt11)
SenderBackend->>LN: Route payment to recipient node
LN->>RecipientNode: Deliver payment
RecipientNode-->>LN: Payment settled (preimage)
LN-->>SenderBackend: Payment confirmed
SenderBackend-->>LightningMediator: {payment_hash}
LightningMediator-->>DrawbridgeAPI: {paymentHash}
DrawbridgeAPI-->>DrawbridgeClient: {paymentHash}
DrawbridgeClient-->>Keymaster: LightningPayment
Keymaster-->>CLI: LightningPayment
CLI->>User: JSON output {paymentHash}