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
Core Lightning node
CLN v23.0+ running and synced:
Connected to Bitcoin Core
Channels funded and active
gRPC interface enabled
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/
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 :
User requests payment : Provides Lightning invoice
Server cosigns HTLCs : Creates HTLC VTXOs locked to payment hash
User registers HTLCs : Completes HTLC VTXOs off-chain
Server pays invoice : Routes via CLN
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 :
User generates invoice : Server creates hold invoice via CLN
Sender pays : Lightning payment routes to CLN
Server holds HTLC : Waits for user to claim
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_i d > < amount_msa t > 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
Configure conservative expiries
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.