Money path. This one changes what the ledger records. Read the money-path
rule in CONTRIBUTING.md
before starting, and expect the pull-request template to ask which of the
nine invariants still hold and why. Labelled help wanted rather than
good first issue for that reason — the code is small, the reasoning is not.
The machinery already exists and only Lightning uses it.
post_settle_entry (src/crypto_processing_api/services/withdrawals.py:493)
takes an optional actual_wallet_fee, books the difference against the
estimate, and logs withdrawal.fee_drift_booked. The capability protocol is
ReportsActualFee (src/crypto_processing_api/services/backends.py:159).
LightningPayoutBackend implements it; BtcpayPayoutBackend does not.
So for on-chain BTC the ledger still books the estimated fee. Estimate and
actual differ on every transaction — fee markets move between quote and mine —
and the difference accumulates in the hot wallet with no entry explaining it.
Job C reports it as an unexplained custody difference, which is
docs/design/06-adversarial-critique.md
items #8 and #9. It is small per withdrawal and it never goes away.
The unknown to resolve first
Does BTCPay Greenfield expose the actual fee of a payout's transaction?
src/crypto_processing_api/gateway/btcpay_client.py reads a fee rate
(get_fee_rate, line 330) and nothing about a mined transaction's fee. The
wallet transactions endpoint is a candidate, as is looking the txid up
elsewhere.
Answering that question with evidence — an endpoint, a real response body from
the regtest stack, and what it contains for a payout — is a genuinely useful
outcome even if no code follows. Post it here.
If the answer is yes
Implement actual_wallet_fee on BtcpayPayoutBackend. Everything downstream
already works: withdrawals.py calls it when the backend has it, and
post_settle_entry books the drift.
- Fee reads happen outside the settle transaction. The lock-decide-commit-
then-call-out rule in CONTRIBUTING is not negotiable in this path.
- Returning
None must stay safe, and must mean "unknown", never "zero". A
missing number that reads as zero is a silent loss.
- Tests:
tests/integration/test_fee_drift.py has the existing shape, and
tests/unit/test_greenfield_fixtures.py shows how recorded real payloads are
asserted rather than invented ones.
If the answer is no
Say so here with the evidence, and this becomes a documentation change instead:
the estimate-versus-actual gap is stated honestly in docs/security.md as a
known, bounded, unexplained drift, rather than left for an operator to
discover from a custody report.
The machinery already exists and only Lightning uses it.
post_settle_entry(src/crypto_processing_api/services/withdrawals.py:493)takes an optional
actual_wallet_fee, books the difference against theestimate, and logs
withdrawal.fee_drift_booked. The capability protocol isReportsActualFee(src/crypto_processing_api/services/backends.py:159).LightningPayoutBackendimplements it;BtcpayPayoutBackenddoes not.So for on-chain BTC the ledger still books the estimated fee. Estimate and
actual differ on every transaction — fee markets move between quote and mine —
and the difference accumulates in the hot wallet with no entry explaining it.
Job C reports it as an unexplained custody difference, which is
docs/design/06-adversarial-critique.mditems #8 and #9. It is small per withdrawal and it never goes away.
The unknown to resolve first
Does BTCPay Greenfield expose the actual fee of a payout's transaction?
src/crypto_processing_api/gateway/btcpay_client.pyreads a fee rate(
get_fee_rate, line 330) and nothing about a mined transaction's fee. Thewallet transactions endpoint is a candidate, as is looking the txid up
elsewhere.
Answering that question with evidence — an endpoint, a real response body from
the regtest stack, and what it contains for a payout — is a genuinely useful
outcome even if no code follows. Post it here.
If the answer is yes
Implement
actual_wallet_feeonBtcpayPayoutBackend. Everything downstreamalready works:
withdrawals.pycalls it when the backend has it, andpost_settle_entrybooks the drift.then-call-out rule in CONTRIBUTING is not negotiable in this path.
Nonemust stay safe, and must mean "unknown", never "zero". Amissing number that reads as zero is a silent loss.
tests/integration/test_fee_drift.pyhas the existing shape, andtests/unit/test_greenfield_fixtures.pyshows how recorded real payloads areasserted rather than invented ones.
If the answer is no
Say so here with the evidence, and this becomes a documentation change instead:
the estimate-versus-actual gap is stated honestly in
docs/security.mdas aknown, bounded, unexplained drift, rather than left for an operator to
discover from a custody report.