Accepting BTC payments means accumulating UTXOs. Every incoming deposit lands in your hot wallet as an individual Unspent Transaction Output — a coin of fixed denomination. A payment processor handling 500 BTC deposits per day will accumulate tens of thousands of UTXOs within months.
That accumulation is invisible until it isn't. A treasury sweep that should cost 200 vbytes costs 8,000 instead because the wallet engine selected 40 small UTXOs to fund it. At 30 sat/vByte on a busy day, the difference between 1 input and 40 is $23 in fee overhead per sweep. At 100 sweeps per day that is $2,300 wasted — not on processing, but on structural debt from a fragmented UTXO set.
This post covers coin selection mechanics, when and how to consolidate, dust thresholds, and the BchainPay API endpoints that expose these controls.
The UTXO model in 90 seconds#
Bitcoin has no account balances. A wallet's balance is the sum of all UTXOs whose locking script it can satisfy. To send funds you select one or more UTXOs as inputs, create an output for the recipient and (optionally) a change output back to yourself, pay the miner the difference as a fee, and sign.
Every input adds weight. For a P2WPKH input (the standard segwit format) that weight is roughly 68 vbytes — 41 for the non-witness fields plus 27 for the witness stack. A P2TR (taproot) input is smaller at about 57.5 vbytes because the witness is more compact. Compare that to a P2WPKH output at 31 vbytes and a fixed transaction overhead of 10.5 vbytes.
The math is brutal: if you have 50 UTXOs of 0.0001 BTC each and need to send 0.005 BTC, you must use all 50 as inputs — 50 × 68 = 3,400 vbytes of input weight before adding a single output. At 50 sat/vByte that is 170,000 sat in fees to move roughly $300 worth of coins.
Coin selection algorithms#
Bitcoin Core ships four coin selection strategies, applied in priority order. Understanding them lets you reproduce or override the behavior in your own infrastructure.
Branch and Bound#
The default first pass since Bitcoin Core 0.17. Branch and Bound (BnB) searches
for an exact match: a subset of UTXOs that sums precisely to
target + estimated_fee with no change output. Exact matches are structurally
more private (no change output to link back to the wallet) and slightly cheaper
(one fewer output saves 31 vbytes).
BnB runs a recursive search capped at 100,000 explored nodes. It scores each candidate subset using a waste metric:
waste = (fee_rate_now - long_term_fee_rate) × input_weight
+ change_cost (0 if no change, otherwise ~32 vbytes at fee_rate_now)
+ excess (value above target paid to miner)
If BnB finds a match inside the node limit, it uses the minimum-waste result. If it exhausts the budget without finding an exact match, it falls through to the next algorithm.
Single Random Draw#
If BnB fails, Bitcoin Core shuffles the UTXO list and greedily selects UTXOs until the target is met. This is run up to 1,000 times and the result with the lowest waste score is kept. SRD almost always produces a change output, which adds ~32 vbytes to every transaction.
Largest-first vs. smallest-first#
Largest-first: sort UTXOs descending by value, pick the fewest inputs needed. Minimizes input count and fees when UTXOs are large relative to the target. This is the strategy to prefer for treasury sweeps where the target amount is large.
Smallest-first: sort ascending, pick until covered. Minimizes input count only when small UTXOs can exactly cover the target; worsens fragmentation otherwise because it preferentially consumes the UTXOs that are cheapest to spend while leaving larger ones untouched.
Sizing your UTXO set#
A healthy hot wallet has two properties:
- Fewer UTXOs than a configurable ceiling so any spend uses 1–5 inputs.
- Larger UTXOs than the dust threshold so even low-value withdrawals avoid ruinous fee ratios.
A rule of thumb: keep your UTXO count below 1.5× your daily outbound transaction count. A processor sending 200 withdrawals per day should maintain
fewer than 300 UTXOs; above that, coin selection degrades and average fees
climb noticeably.
To inspect your current UTXO set:
# Using bitcoin-cli against your own node
bitcoin-cli listunspent 0 9999999 '["bc1qYourHotWalletAddress..."]'
# Scan by descriptor for a whole HD derivation path
bitcoin-cli scantxoutset "start" \
'["wpkh([fingerprint/84h/0h/0h]xpub.../0/*)"]'Via BchainPay:
GET /v1/wallets/btc/utxos
Authorization: Bearer sk_live_…{
"count": 2841,
"total_satoshis": 184920384,
"smallest_sat": 1024,
"largest_sat": 9100000,
"dust_count": 87,
"recommended_consolidation": true
}recommended_consolidation: true fires when the UTXO count exceeds your
configured ceiling (default 500) or the average UTXO value falls below your
configured dust multiplier.
Building a consolidation sweep#
A consolidation transaction spends many inputs to one or a few outputs at a low fee rate. The ideal time to consolidate is during low-mempool periods — weekday nights UTC, or whenever the next block can clear at under 5 sat/vByte.
import * as bitcoin from 'bitcoinjs-lib'
const network = bitcoin.networks.bitcoin
type UtxoInput = {
txid: string
vout: number
valueSat: number
rawTx: string // full hex, for nonWitnessUtxo
}
function buildConsolidationPsbt(
utxos: UtxoInput[],
destinationAddress: string,
feeRateSatVByte: number,
): bitcoin.Psbt {
const psbt = new bitcoin.Psbt({ network })
for (const u of utxos) {
psbt.addInput({
hash: u.txid,
index: u.vout,
// P2WPKH inputs need witnessUtxo; use nonWitnessUtxo for P2PKH / P2SH
nonWitnessUtxo: Buffer.from(u.rawTx, 'hex'),
})
}
// Approximate weight: each P2WPKH input ≈ 68 vbytes, 1 P2WPKH output ≈ 31, overhead ≈ 11
const estimatedVbytes = utxos.length * 68 + 31 + 11
const feeSat = Math.ceil(estimatedVbytes * feeRateSatVByte * 1.1) // +10% margin
const totalIn = utxos.reduce((sum, u) => sum + u.valueSat, 0)
const outputValueSat = totalIn - feeSat
if (outputValueSat < 294) {
throw new Error(`Fee ${feeSat} sat exceeds input value at ${feeRateSatVByte} sat/vByte`)
}
psbt.addOutput({ address: destinationAddress, value: outputValueSat })
return psbt
}Two details matter in production:
Witness vs. non-witness UTXO data. For P2WPKH and P2TR inputs use
witnessUtxo: { script, value } instead of nonWitnessUtxo. BchainPay's UTXO
API returns both the serialized output script and value for each UTXO, so you
always have the right format without an extra RPC round-trip.
Fee margin. The * 1.1 factor accounts for witness discount rounding. An
exact computation is psbt.extractTransaction().virtualSize() after you add a
dummy signature (same byte length as the real one), but the 10% margin is
accurate enough for consolidation transactions where a few hundred sat of
overpayment is irrelevant.
Batch size ceiling#
Bitcoin's block weight limit is 4,000,000 weight units. A P2WPKH input weighs 272 WU (68 vbytes × 4), so the theoretical per-block ceiling is about 14,700 inputs. In practice, keep consolidation batches to 500 inputs or fewer per transaction to avoid propagation issues on nodes with low mempool-acceptance thresholds, and to leave room for a fee-bump replacement if needed.
Handling dust UTXOs#
A UTXO is dust when its value is less than the fee to spend it. Bitcoin Core's relay dust thresholds (at 1 sat/byte minimum relay fee):
| Output type | Dust threshold |
|---|---|
| P2PKH | 546 sat |
| P2WPKH | 294 sat |
| P2TR | 330 sat |
Outputs below these thresholds are rejected by default-policy nodes. Dust arrives from change rounding, tiny micropayments, and address-poisoning probes (attackers dust addresses to deanonymize spending patterns).
Three handling strategies:
Bundle into low-rate consolidations. Collect dust in a separate queue; once the aggregate value covers the consolidation fee at a low rate (≤ 5 sat/vByte), sweep it all in one transaction. Works well when you tolerate waiting for a low-fee window.
Piggyback on outbound transactions. When building any outbound transaction that already needs 2+ inputs, add dust UTXOs as extra inputs if the marginal fee increase is under 20 sat/vByte. You clean up the UTXO set during work you were already doing.
Write off economic dust. UTXOs under ~200 sat at typical fee rates will never be economic to spend. Track them as "stranded balance" in your accounting, exclude them from all coin selection, and revisit if fee rates drop structurally.
Fee-rate gating#
Schedule consolidations on a dual-condition trigger rather than a fixed timer:
async function shouldConsolidate(
utxoCount: number,
currentFeeRateSatVByte: number,
config: { maxUtxos: number; maxConsolidationFeeRate: number },
): Promise<boolean> {
if (utxoCount < config.maxUtxos) return false
if (currentFeeRateSatVByte > config.maxConsolidationFeeRate) return false
return true
}BchainPay's default config is maxUtxos: 500, maxConsolidationFeeRate: 8.
The service polls a mempool fee estimate every 30 minutes and queues a sweep
when both conditions are met. Every consolidation transaction is RBF-enabled
(all inputs set nSequence ≤ 0xFFFFFFFD) so it can be fee-bumped if the
mempool fills unexpectedly before confirmation.
The BchainPay consolidation API#
POST /v1/wallets/btc/consolidate
Authorization: Bearer sk_live_…
Content-Type: application/json
{
"max_fee_rate": 8,
"max_inputs": 300,
"destination": "bc1q...",
"rbf": true,
"dry_run": false
}{
"id": "cons_01HYW…",
"status": "broadcast",
"tx_hash": "a3f1b2c4d5e6…",
"inputs_spent": 287,
"fee_sat": 97580,
"fee_rate_sat_vbyte": 5.1,
"output_sat": 14802420,
"remaining_utxo_count": 31
}dry_run: true returns the estimated fee and vbyte size without broadcasting —
useful for validating your configuration before committing. The remaining_utxo_count
field tells you how many UTXOs will be left after the sweep, so you can confirm
the result lands below your ceiling before execution.
Key takeaways#
- Each Bitcoin deposit creates one UTXO; unmanaged growth inflates coin selection input counts and fees linearly with UTXO count.
- P2WPKH inputs cost ~68 vbytes each; P2TR inputs cost ~57.5 vbytes. Use taproot addresses for new payment flows to reduce long-term fee exposure.
- Branch and Bound finds change-free exact matches and minimizes waste; it is the default in Bitcoin Core 0.17+ and worth implementing in custom gateways.
- Keep hot-wallet UTXO count below 1.5× your daily outbound transaction count; trigger consolidation sweeps when count exceeds that ceiling and the current fee rate is at or below 8 sat/vByte.
- Dust UTXOs (< 294 sat for P2WPKH) should be bundled into low-rate consolidation transactions or piggybacked on existing outbound transactions.
- Always enable RBF on consolidation transactions. A stuck consolidation at 2 sat/vByte during a mempool spike cannot be fee-bumped unless it signals replaceability from the start.