Skip to main content

Overview

captaind integrates with Core Lightning (CLN) to enable Lightning Network payments directly from Ark VTXOs. This allows users to:
  • Send Lightning invoices using their off-chain Ark balance
  • Receive Lightning payments into VTXOs
  • Route payments across multiple CLN nodes

Prerequisites

1

Core Lightning node

CLN v23.0+ running and synced:
  • Connected to Bitcoin Core
  • Channels funded and active
  • gRPC interface enabled
2

Hold Invoice plugin

Install the CLN hold invoice plugin:
# Clone and build the plugin
git clone https://github.com/daywalker90/holdinvoice
cd holdinvoice
cargo build --release

# Copy to CLN plugins directory
cp target/release/holdinvoice ~/.lightning/plugins/
3

TLS certificates

Generate mTLS certificates for gRPC authentication:
# Server certificate (CLN)
openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout server.key -out server.crt -days 365

# Client certificate (captaind)
openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout client.key -out client.crt -days 365

CLN Configuration

Enable gRPC

Add to your CLN config (~/.lightning/config):
# Enable gRPC server
grpc-port=8000
Restart CLN:
lightning-cli stop
lightningd --daemon

Hold Invoice Plugin

The hold invoice plugin should auto-load if placed in the plugins directory. Verify:
lightning-cli plugin list | grep holdinvoice
Expected output:
{
   "name": "holdinvoice",
   "active": true
}

captaind Configuration

Single CLN Node

Add to captaind.toml:
# Lightning timing parameters
cln_reconnect_interval = "10s"
invoice_check_interval = "3s"
invoice_recheck_delay = "2s"
invoice_check_base_delay = "10s"
max_invoice_check_delay = "10m"
invoice_poll_interval = "30s"
track_all_base_delay = "1s"
max_track_all_delay = "60s"

# HTLC timing
htlc_expiry_delta = 40
htlc_send_expiry_delta = 258
max_user_invoice_cltv_delta = 250
invoice_expiry = "48h"
receive_htlc_forward_timeout = "30s"

# Anti-DoS for receives (optional)
ln_receive_anti_dos_required = false

# CLN node configuration
[[cln_array]]
uri = "https://127.0.0.1:8000"  # gRPC endpoint
priority = 10  # Lower number = higher priority
server_cert_path = "/etc/cln/server.crt"
client_cert_path = "/etc/cln/client.crt"
client_key_path = "/etc/cln/client.key"

# Hold invoice plugin endpoint
[cln_array.hold_invoice]
uri = "https://127.0.0.1:8001"  # Hold invoice gRPC port
server_cert_path = "/etc/cln/hold/server.crt"
client_cert_path = "/etc/cln/hold/client.crt"
client_key_path = "/etc/cln/hold/client.key"

Multiple CLN Nodes

For redundancy and load balancing:
# Primary node
[[cln_array]]
uri = "https://cln1.example.com:8000"
priority = 10  # Highest priority
server_cert_path = "/etc/cln/cln1/server.crt"
client_cert_path = "/etc/cln/cln1/client.crt"
client_key_path = "/etc/cln/cln1/client.key"

[cln_array.hold_invoice]
uri = "https://cln1.example.com:8001"
server_cert_path = "/etc/cln/cln1/hold/server.crt"
client_cert_path = "/etc/cln/cln1/hold/client.crt"
client_key_path = "/etc/cln/cln1/hold/client.key"

# Backup node
[[cln_array]]
uri = "https://cln2.example.com:8000"
priority = 20  # Lower priority
server_cert_path = "/etc/cln/cln2/server.crt"
client_cert_path = "/etc/cln/cln2/client.crt"
client_key_path = "/etc/cln/cln2/client.key"

[cln_array.hold_invoice]
uri = "https://cln2.example.com:8001"
server_cert_path = "/etc/cln/cln2/hold/server.crt"
client_cert_path = "/etc/cln/cln2/hold/client.crt"
client_key_path = "/etc/cln/cln2/hold/client.key"
Priority-Based Routing:
  • captaind tries nodes in priority order (lowest first)
  • Falls back to next node if primary is unavailable
  • Rebalances across nodes based on health

Lightning Fees

Configure fees for Lightning operations:
# Receiving Lightning payments
[fees.lightning_receive]
base_fee_sat = 100
ppm = 2000  # 0.2%

# Sending Lightning payments
[fees.lightning_send]
min_fee_sat = 10
base_fee_sat = 75

# Expiry-based PPM (encourages short-lived HTLCs)
ppm_expiry_table = [
    { expiry_blocks_threshold = 0, ppm = 0 },       # ≤96 blocks: 0%
    { expiry_blocks_threshold = 97, ppm = 1000 },   # 97-198: 0.1%
    { expiry_blocks_threshold = 199, ppm = 4000 },  # 199-2160: 0.4%
    { expiry_blocks_threshold = 2161, ppm = 8000 }, # >2160: 0.8%
]
Fee Strategy: Higher fees for long-lived HTLCs incentivize users to use shorter expiries, reducing capital lockup.

How It Works

Sending Lightning Payments

Steps:
  1. User requests payment: Provides Lightning invoice
  2. Server cosigns HTLCs: Creates HTLC VTXOs locked to payment hash
  3. User registers HTLCs: Completes HTLC VTXOs off-chain
  4. Server pays invoice: Routes via CLN
  5. Server claims HTLCs: Uses preimage to claim funds
HTLC VTXO Policy:
VtxoPolicy::ServerHtlcSend {
    payment_hash: Hash,
    htlc_expiry: BlockHeight,
}
Revocation: If payment fails, user can revoke HTLCs to reclaim funds.

Receiving Lightning Payments

Steps:
  1. User generates invoice: Server creates hold invoice via CLN
  2. Sender pays: Lightning payment routes to CLN
  3. Server holds HTLC: Waits for user to claim
  4. User claims: Server creates VTXOs and settles HTLC
Receive VTXO Policy:
VtxoPolicy::ServerHtlcRecv {
    payment_hash: Hash,
    htlc_expiry: BlockHeight,
}
Timeout: If user doesn’t claim within receive_htlc_forward_timeout, server fails HTLC back.

Monitoring

CLN Connection Status

Check active CLN nodes:
# View logs for CLN connections
journalctl -u captaind | grep -i "lightning\|cln"
Metrics:
  • bark_lightning_node: Number of connected CLN nodes (gauge)
  • bark_lightning_node_boot_counter: CLN node reconnections (counter)

Payment Monitoring

Metrics:
  • bark_lightning_payment_counter: Payments by status (success/failed)
  • bark_lightning_payment_volume: Total payment volume in msats
  • bark_lightning_invoice_verification_counter: Invoice checks
  • bark_lightning_open_invoices_gauge: Open invoices

Database Queries

Active invoices:
SELECT * FROM lightning_invoice 
WHERE preimage IS NULL;
Payment attempts:
SELECT 
    li.payment_hash,
    lpa.status,
    lpa.amount_msat,
    lpa.error
FROM lightning_payment_attempt lpa
JOIN lightning_invoice li ON li.id = lpa.lightning_invoice_id
WHERE lpa.status != 'succeeded'
ORDER BY lpa.created_at DESC;
HTLC subscriptions:
SELECT 
    lhs.status,
    li.payment_hash,
    ln.pubkey,
    lhs.accepted_at
FROM lightning_htlc_subscription lhs
JOIN lightning_invoice li ON li.id = lhs.lightning_invoice_id
JOIN lightning_node ln ON ln.id = lhs.lightning_node_id
WHERE lhs.status != 'settled';

Troubleshooting

CLN Connection Failed

Symptom: Failed to connect to CLN node Causes:
  • CLN not running or not synced
  • Incorrect gRPC port
  • TLS certificate mismatch
  • Network firewall blocking connection
Solutions:
# Check CLN is running
lightning-cli getinfo

# Verify gRPC port is open
ss -tlnp | grep 8000

# Test TLS connection
openssl s_client -connect 127.0.0.1:8000 \
  -cert client.crt -key client.key -CAfile server.crt

# Check captaind logs
journalctl -u captaind -n 100 | grep CLN

Hold Invoice Plugin Not Found

Symptom: Hold invoice plugin not available Solutions:
# Check plugin is loaded
lightning-cli plugin list | grep holdinvoice

# Manually load plugin
lightning-cli plugin start ~/.lightning/plugins/holdinvoice

# Check plugin logs
tail -f ~/.lightning/plugins/holdinvoice.log

Payment Failures

Symptom: Lightning payments consistently fail Causes:
  • Insufficient channel liquidity
  • No route to destination
  • CLN node offline
  • Invoice expired
Solutions:
# Check CLN channels
lightning-cli listfunds

# Check route to destination
lightning-cli getroute <node_id> <amount_msat> 1

# Review payment attempts
lightning-cli listpays

# Check captaind payment logs
psql bark-server-db -c "
    SELECT payment_hash, status, error 
    FROM lightning_payment_attempt 
    WHERE status = 'failed' 
    ORDER BY created_at DESC LIMIT 10;
"

Receive Timeouts

Symptom: Users can’t claim received payments Causes:
  • User offline during receive_htlc_forward_timeout
  • Database connectivity issues
  • Server overloaded
Solutions:
  • Increase receive_htlc_forward_timeout to 60s+
  • Optimize database queries
  • Check server resources (CPU, memory)
  • Review captaind logs for errors

Best Practices

For production:
  • At least 2 CLN nodes for redundancy
  • Different network paths if possible
  • Monitor both nodes independently
  • Set appropriate priority values
  • Maintain balanced channels (inbound + outbound)
  • Use submarine swaps or loop for rebalancing
  • Monitor channel health and closure rates
  • Set fee policies to encourage balanced flow
  • Use high htlc_expiry_delta (40+ blocks) for safety
  • Set max_user_invoice_cltv_delta appropriate for your routes
  • Allow generous receive_htlc_forward_timeout for UX
Set up alerts for:
  • Payment success rate < 95%
  • Invoice claim rate < 90%
  • CLN node disconnections
  • Channel force closures

Advanced Configuration

Custom Invoice Expiry

# Default invoice expiry
invoice_expiry = "48h"

# For faster payments, use shorter expiry
# invoice_expiry = "15m"

Anti-DoS for Receives

# Require users to prove VTXO ownership OR provide token
ln_receive_anti_dos_required = true
Prevents spam invoice generation.

Invoice Polling Tuning

# Base delay for checking invoice status
invoice_check_base_delay = "10s"

# Max delay (exponential backoff)
max_invoice_check_delay = "10m"

# Recheck delay (minimum time between checks)
invoice_recheck_delay = "2s"
Reduces CLN RPC load while maintaining responsiveness.

Build docs developers (and LLMs) love