For the complete documentation index, see llms.txt. This page is also available as Markdown.

How to Plan and Price a Bitcoin Spend with GetBlock's Blockbook Add-on

Build a Bitcoin coin selector and fee planner on GetBlock's Blockbook add-on: choose which unspent outputs to spend, and price the transaction before signing.

Bitcoin has no account balances. A wallet's balance is a fiction; it computes by adding up unspent transaction outputs, and to send a payment it must choose which of those outputs to spend, a decision with no obvious right answer and real money riding on it.

The choice is circular: the fee is a price per virtual byte, so it depends on the transaction's size, the size depends on how many inputs it has, and how many inputs it needs depends on the fee. Worse, some outputs only look spendable — a mining reward is locked by consensus for 100 blocks, and an output worth less than the fee to spend it is stranded value.

Get any of this wrong, and you produce a transaction that overpays by an order of magnitude, cannot be relayed, or cannot be mined at all.

In this guide, you will learn how to decide which unspent outputs to spend for a Bitcoin payment and what that payment will cost, using GetBlock's Blockbook add-on.

What you'll build

A planAt(spendable, { amount, satPerKvB, inputType, toType, changeType }) function that:

  1. Converts every output to its effective value — its worth minus the fee for its own input — which removes the circularity between fee and input count.

  2. Drops outputs that cost more to spend than they hold, and reports how much value that strands.

  3. Searches for a changeless combination that lands just above the target, so the transaction needs no change output at all.

  4. Falls back to largest-first and random-draw selection when no changeless combination exists.

  5. Prices each candidate plan, dropping a change output that would be dust and handing the leftover to the miner instead.

  6. Ranks the plans by total cost — the fee plus what the change it leaves behind will cost to spend a day on.

Prerequisites

  • Node.js 20.6 or later — for the built-in fetch and --env-file, so there is nothing to install.

  • A Bitcoin endpoint with the Blockbook add-on enabled (REST). Blockbook keeps the address-indexed view of the chain that a plain Bitcoin node does not.

  • An address to plan against. No wallet or funds needed: the guide points at a busy public address whose UTXO set is fragmented and dust-heavy, which is exactly what makes coin selection worth watching.

  • Basic JavaScript knowledge.

Everything here is a GET request. Nothing in this guide needs a private key, and nothing it produces can move coins — turning a plan into a transaction means building a PSBT and signing it, which this program never does.

Project Setup

1

Get the REST endpoint

In the GetBlock dashboard, create a Bitcoin mainnet endpoint with the Blockbook add-on. Copy the URL the dashboard gives you exactly as it appears — it will look like one of these:

2

Scaffold the project

3

Add your configuration

Create a .env file:

4

Handle satoshis, and size a transaction

Create plan.js.

Start with the arithmetic and the sizing, because a planner that cannot size a transaction cannot price one.

5

Talk to Blockbook

Two helpers: one that fetches, and one that converts fee estimates to satoshis.

Reusing toSats on the estimator's reply is the whole unit conversion: a BTC decimal string becomes an integer count of satoshis per kilo-virtual-byte, and feeFor divides by a thousand at the point of use.

6

Select the coins

This is the heart of it. Three strategies, all working in effective value.

7

Price each plan, and rank them

Selection picks the inputs; costing decides what happens to the leftover.

Notice what the two targets do. targetWithChange and targetChangeless are computed with zero inputs — they cover only the overhead and the outputs. Because each candidate has already paid for its own input inside its effective value, the target never moves as inputs are added. That is what breaks the circularity, and it is why the leftover at the end is exactly the change.

8

Classify the outputs and report

The last piece is the part most easily skipped: two kinds of output look spendable in a balance and are not.

The full reporting code — the fee-tier table, the strategy comparison and the sensitivity table — is in the project repo.

9

Run it

Two things are already visible. The address holds 0.0954 BTC but only 0.0230 BTC of it is spendable — 122 of its 270 outputs are still in the mempool. And a 0.0015 BTC payment requires 10 inputs because this wallet has no large outputs to spend.

At this fee rate, the change is nearly free, so keeping it wins. Force a busy mempool and the answer changes:

The changeless plan now wins, twelve outputs have dropped out of play entirely, and the fee to send 0.0015 BTC is 0.00276 BTC — nearly twice the payment. That is the true cost of a fragmented wallet, and it only becomes visible when fees rise.

Push further and the wallet stops working altogether:

Every output is worth less than the 20,400 sats its own input would cost. The balance is real, the keys work, and not one satoshi can move.

10

Watch the coinbase rule bite

Point the planner at a mining address to see the other exclusion. This one comes from a block's coinbase transaction:

Of 226 BTC held, 77 BTC is unspendable — 28 recent mining rewards still inside their 100-block maturity window. Spending one is not merely unwise; consensus rejects the transaction.

The changeless search also reports a miss here, and that is the honest answer rather than a bug: a wallet holding a few very large outputs has no subset that falls within 100 satoshis of the target. This is exactly why a real wallet needs fallback strategies.

Treat the vsize and fee in this particular example as indicative rather than exact. This address is P2WSH, and a P2WSH input's witness is a script that can be almost any size — the 105 vB in SCRIPTS is a reasonable stand-in for a small multisig, not a universal figure. The single-key types (P2PKH, P2SH-P2WPKH, P2WPKH, P2TR) are accurate.

Understanding the response

A /api/v2/utxo/{address} entry describes one unspent output:

Field
Type
What it tells you

txid

string

The transaction that created this output. Half of the outpoint that identifies it.

vout

integer

Which output of that transaction. The other half of the outpoint.

value

string

The amount, in satoshis as a decimal string. Parse it with BigInt, never Number.

height

integer

The block that confirmed it. Absent while the output is unconfirmed.

confirmations

integer

How many blocks bury it. 0 means it is still in the mempool, and inherits its parent's replaceability.

coinbase

boolean

Present and true for a mining reward. Unspendable until 100 confirmations, by consensus.

Fields are omitted rather than zeroed when they do not apply: an unconfirmed output has no height, and coinbase appears only on mining rewards. Treat every field as optional and default it, which is what utxo.confirmations ?? 0 is doing.

And the two fee endpoints:

Field
Type
What it tells you

result

string

estimatefee — the feerate in BTC per kilo-virtual-byte. Multiply by 100,000,000 for sat/kvB, then divide by 1,000 for sat/vB.

averageFeePerKb

integer

feestats — the mean feerate a block actually paid, in satoshis per kvB. Different unit from result above.

decilesFeePerKb

array

Eleven feerates from cheapest to dearest transaction in that block, in sat/kvB. Index 5 is the median.

txCount

integer

Transactions in the block, which tells you how much the deciles are worth trusting.

Troubleshooting

Symptom
Likely cause
Fix

npm start prints the usage message even though .env is filled in

npm keeps the arguments for itself, so the amount never reaches the script.

npm start -- 0.0015 — the -- passes the rest through. Or call node --env-file=.env plan.js 0.0015 directly.

The response is HTML, not JSON

The path was wrong, so Blockbook served its explorer page instead of the API.

Check the path starts /api/v2/. A 200 with <!doctype html> is a routing mistake, not a chain error.

Fees are roughly a thousand times too high or too low

estimatefee returns BTC per kilo-virtual-byte, and it is easy to read as sat/vB or BTC/vB.

"0.00000490" is 490 sat/kvB, so 0.49 sat/vB. Convert once, in one place.

estimatefee returns Internal server error

The confirmation target is outside the node's horizon of 1–1008 blocks.

Ask for a target in range. The error is bare, so treat any failure as "no estimate for this tier" and carry on.

Invalid address '...', checksum mismatch

A typo in the address, or a network mismatch such as a testnet address on a mainnet endpoint.

Check the address, and check the endpoint is for the chain you meant.

The plan is rejected by the network as non-standard

A change output below the dust threshold.

Compare change against (output size + input size) × 3 sat — the dust relay fee is 3 sat/vB by default — and drop the output if it falls short, letting the leftover become fee.

A plan is never mined

It included an immature coinbase output, or an unconfirmed one whose parent was replaced.

Exclude coinbase outputs under 100 confirmations, and require at least one confirmation for the rest.

The fee is larger than the payment

The wallet is fragmented, so the payment needs many inputs, and each input costs 68 vB.

This is a real result, not a bug. Consolidate when fees are low, so a payment does not need forty inputs when they are high.

Balance looks right but nothing can be spent

Every output is worth less than the fee for its own input at the current feerate.

Wait for cheaper fees. Value in outputs smaller than their own input cost is stranded, not lost.

Confirmation counts look stale

Blockbook's index has fallen behind its node.

Compare blockbook.bestHeight with backend.blocks in /api/v2. The inSync flag can read true while the index sits a block behind.

Where to take it

Three changes turn this into a wallet's selection layer.

  1. Plan across the whole wallet, not a single address: The same /api/v2/utxo/ path accepts an xpub, ypub or zpub in place of an address and returns the unspent outputs across every derived address, so selection runs over the real wallet rather than one slice of it. Selection logic is unchanged; what changes is that each chosen input has to carry the derivation path its signer will need, so check which fields your endpoint returns for an xpub before relying on them.

  2. Mix script types honestly: The sizing here assumes that every input is the same type, which is true for a single address but false for a real wallet. Carry a per-input vsize instead of a single inputType, and compute each input's effective value from its own cost — a Taproot input at 58 vB and a legacy input at 148 vB are worth genuinely different amounts of the same nominal value.

  3. Turn the plan into a PSBT: The plan already names the outpoints, the amounts and the change; a PSBT is that plus the metadata a signer needs. Building one keeps the split this guide relies on — the process that chooses coins never has to hold a key.

Two limits worth knowing. The changeless search is capped at 100,000 attempts, so on a large UTXO set it may miss a combination that exists; Bitcoin Core's version also minimizes long-term waste rather than immediate excess, which matters when today's feerate is unusually far from normal. And every plan is a snapshot: an output can be spent by another process between the query and the broadcast, so a production wallet locks the outputs it has selected and re-checks before signing.

Conclusion

You built a Bitcoin coin selector and fee planner in one dependency-free file, using Blockbook's UTXO and fee-estimate endpoints to answer the question a wallet must answer before it can sign: which outputs to spend, and at what cost. The ideas that made it work were switching to effective value so each input pays for its own fee and the target stops moving, excluding outputs that consensus or economics put out of reach, searching for a combination that needs no change at all, dropping change that would be dust, and comparing plans by total cost rather than by fee. None of it needed a private key.

Resources

Last updated

Was this helpful?