DEV Community

Cover image for Building an Anchor Output Fee Bumping Service — Part 4: Lightning Payment Integration
Susan Githaiga
Susan Githaiga

Posted on

Building an Anchor Output Fee Bumping Service — Part 4: Lightning Payment Integration

In Part 3, we built a service that could watch the Bitcoin mempool, spot anchor outputs, and estimate whether a fee bump was feasible. It was a solid foundation, but it stopped short of actually doing anything. No transaction was built. No fee was bumped. And crucially, no one was paying for the service.

Part 4 fixes all of that.

By the end of this article, the service will generate Lightning invoices, verify payments, construct real CPFP transactions using bitcoinjs-lib, and only broadcast those transactions once payment is confirmed. In other words: a working, payment-gated fee bumping service.

We are using Bitcoin regtest, so there's no risk to real funds. The setup is similar to how transactions would work on the mainnet network.

📂 All source code for this project is available here: Anchor Outputs fee-bumping service

File paths referenced throughout this article correspond directly to this repository.


Recap: Where Part 3 Left Us

Before diving in, here's a quick summary of what was already working:

  • Bitcoin Core RPC integration: the service could connect to a regtest node, query the mempool, and fetch transaction details
  • Anchor output detection: any 330-satoshi output in the mempool was flagged as a potential anchor
  • Fee estimation: given a target fee rate, the service calculated whether the 330-sat anchor alone was enough, or whether additional wallet inputs would be needed
  • Basic REST API: endpoints to start/stop monitoring and request fee estimates

What was missing was everything that comes after the estimate: building the child transaction, collecting payment, and broadcasting. That's what we're adding now.


What We're Adding in Part 4

Here's what's new:

  • Lightning invoice generation: charge users for the fee bump service
  • Payment verification: confirm the invoice is settled before taking action
  • CPFP transaction builder: construct the actual child transaction using bitcoinjs-lib
  • Payment-gated broadcast: only broadcast the CPFP after payment is confirmed

The end result is a complete payment flow: a user's stuck transaction gets bumped, and they pay for it instantly over Lightning.

The New Payment Flow

Before walking through the code, it helps to see how all these pieces connect. Here is the complete flow a user goes through:

Bitcoin CPFP tutorial

The key design decision here is that nothing irreversible happens until payment is confirmed. The service checks the payment hash before building or broadcasting anything. This is what makes it viable as a service rather than just a dev tool.


Starting the Implementation

Step 1: Installing New Dependencies

The CPFP transaction builder needs a few new packages for constructing and signing Bitcoin transactions.

Run the following in your project root:

npm install bitcoinjs-lib tiny-secp256k1 ecpair
Enter fullscreen mode Exit fullscreen mode

What each package does:

Package Purpose
bitcoinjs-lib Builds and encodes Bitcoin transactions
tiny-secp256k1 Low-level elliptic curve cryptography (required by bitcoinjs-lib v6+)
ecpair Key pair management for signing transactions

Before Testing: Ensuring LND is Synced

Before testing invoice creation, you need to ensure both Bitcoin Core and LND are properly synced. This is a common issue when working with regtest that can cause invoice creation to hang indefinitely.

Step 1: Load Bitcoin Wallet

After restarting Docker containers, Bitcoin Core's wallet needs to be loaded:

# Load the wallet
docker compose exec bitcoin bitcoin-cli -regtest \
  -rpcuser=bitcoinrpc -rpcpassword=changeme \
  loadwallet "testwallet"
Enter fullscreen mode Exit fullscreen mode

Expected response:

{
  "name": "testwallet"
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Generate Blocks to Complete Sync

Bitcoin Core may still be in Initial Block Download (IBD) mode, which prevents LND from syncing. Generate additional blocks to complete the sync:

# Generate 100 blocks
docker compose exec bitcoin bitcoin-cli -regtest \
  -rpcuser=bitcoinrpc -rpcpassword=changeme \
  -generate 100
Enter fullscreen mode Exit fullscreen mode

Step 3: Verify LND Sync Status

Wait approximately 10 seconds after generating blocks, then check if LND has synced to the chain:

# Check LND sync status
docker compose exec lnd lncli --network=regtest getinfo | \
  jq '{synced_to_chain, block_height}'
Enter fullscreen mode Exit fullscreen mode

Sample response:

Check LND sync status

The critical field is "synced_to_chain": true. If this shows false, LND is still waiting for Bitcoin Core to sync, and invoice creation will hang.

If sync is still false:

  • Wait another 10-20 seconds
  • Generate more blocks: docker compose exec bitcoin bitcoin-cli -regtest -rpcuser=bitcoinrpc -rpcpassword=changeme -generate 50
  • Check LND logs: docker compose logs lnd --tail=20

Once synced_to_chain is true, you're ready to test invoice creation.


Step 2: The Lightning Payment Service

File: src/services/lightning/payment.ts

This service handles all communication with LND's REST API. It's intentionally kept separate from the existing lnd.ts, which only handles node info; one file for node status, one for payment operations.

The service does three things:

Invoice creation: given an amount in sats, a memo, and an expiry time, it calls LND's /v1/invoices endpoint and returns a payment_request string the user can pay from any Lightning wallet.

Payment status checks: given a payment hash, it queries LND to see whether that invoice has been settled. One important detail here: LND's REST API expects the payment hash encoded as base64, but Bitcoin tools generally work in hex. The service handles that conversion internally.

Payment polling: rather than setting up a webhook infrastructure, the service simply checks payment status every 2 seconds until it's confirmed or a timeout is reached. This is simpler for regtest and works reliably, though a production system would benefit from webhooks for efficiency.

A note on authentication: LND uses macaroons , similar to API tokens, to authorize requests. The service reads the macaroon file from disk on startup and attaches it to every request as a header. In regtest, LND also uses a self-signed TLS certificate, so the HTTPS agent is configured to allow that. This should be handled properly with real certificates in production.


Step 3: Exposing Lightning Endpoints

File: src/api/v1/lightning-payment.ts

With the service built, three REST endpoints expose its functionality:

Method Endpoint What it does
POST /api/v1/lightning/create-invoice Generates a Lightning invoice for a given amount
GET /api/v1/lightning/payment/:hash Checks whether a specific invoice has been paid
POST /api/v1/lightning/decode-invoice Returns the decoded details of a payment request string

These are registered in src/index.ts under the /api/v1/lightning path.

Testing Invoice Creation

Create an invoice:

curl -X POST http://localhost:3000/api/v1/lightning/create-invoice \
  -H "Content-Type: application/json" \
  -d '{"amountSats": 2500, "memo": "Test"}' | jq
Enter fullscreen mode Exit fullscreen mode

A successful response includes a payment_request (invoice) field starting with lnbcrt (the regtest prefix) and a paymentHash which you'll use to check status later:

Terminal showing the curl command and the response with payment_request and paymentHash fields visible

Testing Payment Status Check

Check payment status:

# Replace with your actual payment hash
curl http://localhost:3000/api/v1/lightning/payment/702085df5684eab92460a1dbd9da797015d3fffcc179290f22c12b32facebbea | jq
Enter fullscreen mode Exit fullscreen mode

Before payment, this returns "paid": false:

Payment status showing paid: false

Testing Limitation on Regtest

On regtest with a single LND node and no open channels, you cannot actually pay invoices. In production, users would pay from their Lightning wallets, but for testing purposes, we can verify the broadcast logic works correctly.

The payment verification logic works. The service correctly checks payment status before broadcasting.

To fully test the payment flow in a real environment, you would need:

  • A second Lightning node (or use a testnet/mainnet Lightning wallet)
  • An open channel between the nodes
  • Sufficient balance to pay the invoice

For the purposes of this tutorial, we've verified that:

  • Invoice creation works ✓
  • Payment status checking works ✓
  • Broadcast is gated by payment verification ✓

Next: Part 5 continues with the CPFP transaction builder and the payment-gated broadcast endpoint.

Top comments (0)