Runbook: moving value between the on-chain and Lightning floats¶
BTC and BTC_LN are two separate custodial floats. On-chain BTC lives in
BTCPay's hot wallet; Lightning BTC is channel balance. They are the same asset
to a user's eye and different money to yours, and nothing in this service moves
value between them. That is a deliberate omission — an automated path between
two floats is an automated path to drain both.
This runbook is what you do instead. It is short because the mechanism is BTCPay's, not ours; the parts worth writing down are the ones about the ledger.
When you need it¶
Two signals, and they mean opposite things.
Outbound liquidity is running low. GET /v1/admin/reconciliation shows the
BTC_LN line's chain_balance — that is outbound channel liquidity, the only
Lightning money that can pay a withdrawal. When it drops toward
user_obligations, withdrawals stop routing.
What that looks like from the outside is worth knowing in advance, because none
of it is an error message. A withdrawal reaches broadcast and stays there:
BTCPay has taken the payout and cannot route it, and it will not mark it failed
or let it be cancelled. After LN_PAYOUT_TIMEOUT_SECONDS you get one
withdrawal.hold_needs_attestation alert. The balance stays held until the
user's invoice expires, at which point the hold is returned automatically with a
proof recorded on the row. A run of those is telling you the channel is empty —
not that anything is broken.
You do not have to wait for the invoice. An admin who has checked can release
sooner with POST /v1/admin/withdrawals/{id}/release, and the honest
attestation is that the node reports the payment failed and the channel has no
outbound capacity to retry with.
Inbound liquidity is running low. Nothing alerts on this, because it is not
a solvency problem — it is a deposits problem. Symptom: users report that paying
your Lightning invoice fails. chain_balance will look healthy while it
happens, because a full channel on our side is exactly what a lot of successful
deposits produce.
Moving on-chain BTC into a channel¶
- Read both floats first:
GET /v1/admin/reconciliation, theBTCandBTC_LNlines. Write downchain_balancefor each. - In BTCPay's UI, open a channel from the store's Lightning node, funded from the on-chain wallet, or add to an existing one.
- Wait for the funding transaction to confirm and the channel to become active.
- Re-read
/v1/admin/reconciliation.
What you will see, and why it is correct. The BTC line's chain_balance
is now lower than ledger_custody, so difference is negative. The BTC_LN
line's is higher, so its difference is positive. Neither is an error: the
ledger says nothing happened because, from a user's point of view, nothing did.
No user gained or lost a satoshi, so no user account moved.
If the on-chain side is now below user_obligations, the report will say
insolvent: true for BTC and raise custody.insolvency_signal. That is the
check working. You have funded a channel with money you owe on-chain
depositors. Close the channel or top the wallet back up.
Booking it, if you want the books to say so¶
You do not have to. The two difference figures are self-explanatory if you
know a rebalance happened, and this service alerts rather than repairing on
purpose.
If you would rather the ledger carried the movement, it is two adjustment
entries against external — one per asset, because an entry is per asset and a
rebalance is by definition two assets:
Both sum to zero on their own asset, which is what the zero-sum trigger checks.
Use the same memo on both so they can be read as one movement. There is no
endpoint for this; it is a post_entry call from a console, and it is the only
thing in this document that touches the ledger.
An EntryKind.REBALANCE that made this a first-class operation is a plausible
future change. It is not here because a movement nobody has automated does not
need a vocabulary yet.
What not to do¶
- Do not withdraw from one float to fund the other through the public API. It would work, and it would charge a user's balance for an operator's action.
- Do not size a channel from the daily cap. The cap bounds a day of user withdrawals; channel capacity is locked up until the channel closes. They are different questions with different answers.
- Do not close a channel to fix an insolvency alarm on
BTC_LN. Closing returns the funds on chain, which fixes the number by removing the ability to pay any Lightning withdrawal at all. If users are owed Lightning balances, they still are.