-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
This page explains the technical architecture of the BTCPay Server CLINK plugin.
The plugin operates at two levels:
-
Server-side (.NET): Implements the BTCPay Server
ILightningClientinterface, making CLINK appear as a native Lightning backend - 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 │
└──────────────┘ └──────────────────┘ └─────────────────┘
When a customer creates an invoice in BTCPay Server:
- BTCPay calls
ClinkLightningClient.CreateInvoice() - The plugin spawns a Node.js process running the Nostr bridge
- The bridge encodes a kind 21001 Nostr event (NIP-44 encrypted) containing:
- The merchant's offer data
- Invoice amount in sats
- Description
- Expiry time
- The event is published to the merchant's primary Nostr relay
- The bridge subscribes for a response event (also kind 21001, tagged with the request event ID)
- The merchant's Lightning node decrypts the request, generates a BOLT11 invoice, and sends it back as a kind 21001 response
- The plugin receives the BOLT11 and returns it to BTCPay Server
- BTCPay Server displays the Lightning invoice as a QR code
- The customer scans the QR with their Lightning wallet (ShockWallet, ZEUS, etc.)
- The customer pays the BOLT11 invoice
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
| 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.
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.
| 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 |
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.
The checkout page includes clink-payment.js, an ES module that uses @shocknet/clink-sdk to:
- Request invoices directly from the merchant's Nostr relay (bypassing the server-side bridge)
- Display QR codes for payment
- Handle "Open in Wallet" deep links
- Poll for payment status
This dual approach (server-side bridge + client-side SDK) provides flexibility and redundancy.
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.
Maps BTCPay invoice IDs to nDebit strings for subscription auto-pay. Persisted to clink-ndebit-registry.json.
Maps buyer emails to nDebit strings per store, using BTCPay's IStoreRepository for persistence. Enables returning customers to auto-pay future subscription invoices.
The plugin enforces a minimum of 10 sats for Lightning invoices. Requests below this threshold will throw an ArgumentOutOfRangeException.