Skip to content

How It Works

WoompaLoompa edited this page Jul 12, 2026 · 1 revision

This page explains the technical architecture of the BTCPay Server CLINK plugin.

Architecture Overview

The plugin operates at two levels:

  1. Server-side (.NET): Implements the BTCPay Server ILightningClient interface, making CLINK appear as a native Lightning backend
  2. Client-side (JavaScript): A self-contained ES module runs in the customer's browser during checkout
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  BTCPay       β”‚     β”‚  CLINK Plugin     β”‚     β”‚  Nostr Relays   β”‚
β”‚  Server       │◄───►│  (.NET + Node.js) │◄───►│                 β”‚
β”‚               β”‚     β”‚                   β”‚     β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                      β”‚                        β”‚
       β”‚                      β”‚                        β”‚
       β–Ό                      β–Ό                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Customer     β”‚     β”‚  Merchant's       β”‚     β”‚  CLINK-compatibleβ”‚
β”‚  Browser      β”‚     β”‚  Lightning Node   β”‚     β”‚  Wallet          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Payment Flow

Step 1: Invoice Creation

When a customer creates an invoice in BTCPay Server:

  1. BTCPay calls ClinkLightningClient.CreateInvoice()
  2. The plugin spawns a Node.js process running the Nostr bridge
  3. The bridge encodes a kind 21001 Nostr event (NIP-44 encrypted) containing:
    • The merchant's offer data
    • Invoice amount in sats
    • Description
    • Expiry time
  4. The event is published to the merchant's primary Nostr relay
  5. The bridge subscribes for a response event (also kind 21001, tagged with the request event ID)
  6. The merchant's Lightning node decrypts the request, generates a BOLT11 invoice, and sends it back as a kind 21001 response
  7. The plugin receives the BOLT11 and returns it to BTCPay Server

Step 2: Customer Payment

  1. BTCPay Server displays the Lightning invoice as a QR code
  2. The customer scans the QR with their Lightning wallet (ShockWallet, ZEUS, etc.)
  3. The customer pays the BOLT11 invoice

Step 3: Payment Confirmation

The plugin detects payment through two mechanisms:

Server-side polling (primary):

  • ClinkLightningClient.GetInvoice() is called periodically by BTCPay Server
  • The plugin calls CheckPayment() on the Nostr bridge
  • The bridge subscribes to kind 21001/21002 events from the merchant's relay, looking for a res: "ok" response (NIP-44 encrypted)
  • When found, the invoice is marked as paid

Listener (alternative):

  • ClinkInvoiceListener.WaitInvoice() polls every 3 seconds
  • Iterates through all unpaid invoices and checks Nostr for payment receipts
  • Returns immediately when a payment is detected

Nostr Event Types

Kind Direction Purpose
21001 Customer β†’ Merchant Request a Lightning invoice
21001 Merchant β†’ Customer Response with BOLT11 invoice
21002 Customer β†’ Merchant Payment confirmation receipt

All events are NIP-44 encrypted using conversation keys derived from the ephemeral customer keypair and the merchant's public key.

Nostr Bridge

The Nostr bridge (clink-bridge.bundle.mjs) is a Node.js script embedded in the plugin DLL as an embedded resource. On startup, ClinkNostrBridge extracts it to a temp directory and spawns node processes for each command.

Bridge Commands

Command Purpose Timeout
request-invoice Request a BOLT11 from the merchant's node 60s
check-payment Check if an invoice has been paid 30s
pay-invoice Pay an invoice via nDebit 45s

Communication Protocol

The bridge communicates via stdin/stdout JSON:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    stdin (JSON)    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ClinkNostrBridge β”‚ ───────────────► β”‚  clink-bridge     β”‚
β”‚  (.NET)          β”‚ ◄─────────────── β”‚  (Node.js)        β”‚
β”‚                  β”‚    stdout (JSON)  β”‚                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The .NET process writes a JSON payload to the bridge's stdin, closes stdin, and reads the JSON response from stdout. If the process doesn't exit within 120 seconds, it is killed and a TimeoutException is thrown.

Client-Side SDK

The checkout page includes clink-payment.js, an ES module that uses @shocknet/clink-sdk to:

  1. Request invoices directly from the merchant's Nostr relay (bypassing the server-side bridge)
  2. Display QR codes for payment
  3. Handle "Open in Wallet" deep links
  4. Poll for payment status

This dual approach (server-side bridge + client-side SDK) provides flexibility and redundancy.

Data Storage

NostrEventStore

In-memory + file-backed store mapping BTCPay invoice IDs to Nostr event data:

  • Event ID (Nostr event hash)
  • BOLT11 (Lightning invoice)
  • Amount (sats)
  • Created/Paid timestamps
  • Ephemeral keypair (for NIP-44 decryption)

Persisted to clink-nostr-store.json in the plugin directory.

NdebitRegistry

Maps BTCPay invoice IDs to nDebit strings for subscription auto-pay. Persisted to clink-ndebit-registry.json.

EmailNdebitStore

Maps buyer emails to nDebit strings per store, using BTCPay's IStoreRepository for persistence. Enables returning customers to auto-pay future subscription invoices.

Minimum Invoice Amount

The plugin enforces a minimum of 10 sats for Lightning invoices. Requests below this threshold will throw an ArgumentOutOfRangeException.

Clone this wiki locally