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