A Bitcoin PSBT transaction builder I built for the Summer of Bitcoin 2026 challenge (Week 2).
Coin Smith takes a set of UTXOs, payment targets, and a fee rate β then selects coins, computes fees/change, and produces a fully valid unsigned PSBT (BIP-174). It ships with a CLI for machine-checkable output and a web visualizer that explains everything in plain English.
πΉ Watch the walkthrough on YouTube (< 2 min)
- Parses fixture inputs defensively β rejects malformed UTXOs, addresses, amounts
- Implements coin selection strategy (greedy) with support for
policy.max_inputsenforcement - Computes accurate vbytes estimation across all script types (P2PKH, P2SH-P2WPKH, P2WPKH, P2WSH, P2TR)
- Calculates fees from target
fee_rate_sat_vbΓ estimated vbytes - Smart change handling β only creates change when it's above the dust threshold (546 sats)
- Handles edge cases: send-all (no change), dust change absorption, fee/change boundary conditions
- Builds valid BIP-174 PSBTs with
bitcoinjs-lib - Includes
witness_utxometadata for each input - Correct
nSequenceandnLockTimeper BIP-125 (RBF) and locktime rules - Anti-fee-sniping support (
nLockTime = current_heightwhen applicable) - Base64 encoded output ready for hardware wallet signing
- Full RBF signaling via
nSequence(BIP-125) - Absolute locktime support (block height and unix timestamp)
- Anti-fee-sniping: sets
nLockTime = current_heightwhenrbf: trueand no explicit locktime - Correct interaction matrix between
rbf,locktime, andcurrent_heightfields
HIGH_FEEβ fee > 1M sats or rate > 200 sat/vBDUST_CHANGEβ change output below 546 satsSEND_ALLβ no change created, leftover consumed as feeRBF_SIGNALINGβ transaction opts into Replace-By-Fee
- Dark-themed UI with clean layout
- Paste fixture JSON to build a PSBT instantly
- Visual breakdown of selected inputs β payment outputs + change
- Fee, fee rate, vbytes display
- RBF signaling indicator and locktime info
- Warning badges with explanations
- Health endpoint at
GET /api/health
coin-smith/
βββ src/
β βββ cli.ts # CLI entry point β fixture β JSON report
β βββ coin-selection.ts # UTXO selection algorithm
β βββ psbt-builder.ts # PSBT construction with bitcoinjs-lib
β βββ rbf-locktime.ts # RBF signaling & locktime logic
β βββ weight.ts # vbytes estimation per script type
β βββ validate.ts # Input validation & error handling
β βββ warnings.ts # Warning detection (high fee, dust, etc.)
β βββ types.ts # TypeScript interfaces
β βββ server.ts # Express server for web UI
β βββ __tests__/
β βββ coin-selection.test.ts
β βββ rbf-locktime.test.ts
β βββ validate.test.ts
β βββ weight.test.ts
βββ public/
β βββ index.html # Web UI
β βββ style.css # Styling (dark theme)
β βββ app.js # Frontend logic & rendering
βββ fixtures/ # 24 test fixtures (various scenarios)
βββ cli.sh # CLI runner script
βββ web.sh # Web server runner script
βββ setup.sh # Dependency installer
- Node.js (v18+)
- npm
bash setup.sh# Basic P2WPKH transaction with change
bash cli.sh fixtures/basic_change_p2wpkh.json
# Send-all (no change)
bash cli.sh fixtures/send_all_dust_change.json
# RBF with locktime
bash cli.sh fixtures/rbf_with_locktime.jsonOutput goes to out/<fixture_name>.json.
npm testbash web.sh
# β http://127.0.0.1:3000-
Validation β Parse the fixture JSON and validate every field: UTXOs must have valid txids (64 hex chars), amounts must be positive, scriptPubKeys must match claimed script types, addresses must be valid for the network.
-
Coin Selection β I use a greedy algorithm that sorts UTXOs by value (largest first) and selects until we have enough to cover payments + estimated fee. It respects
policy.max_inputsif set. -
Fee Calculation β Estimate vbytes based on input/output script types (each type has a known weight contribution). Fee =
ceil(fee_rate Γ vbytes). The tricky part is that adding/removing a change output changes the vbytes, which changes the fee β so it requires iteration. -
Change Logic β If
inputs - payments - fee β₯ 546(dust threshold), create a change output. Otherwise, the leftover gets absorbed into the fee (send-all). Never create dust outputs. -
PSBT Construction β Using
bitcoinjs-lib, build the unsigned transaction with correctnVersion,nLockTime, and per-inputnSequencevalues. Attachwitness_utxodata for each input. Serialize as base64. -
RBF/Locktime β Follow the interaction matrix between
rbf,locktime, andcurrent_heightto set the correctnSequenceandnLockTimevalues.
{
"ok": true,
"network": "mainnet",
"strategy": "greedy",
"selected_inputs": [...],
"outputs": [
{ "n": 0, "value_sats": 70000, "script_type": "p2wpkh", "is_change": false },
{ "n": 1, "value_sats": 29300, "script_type": "p2wpkh", "is_change": true }
],
"change_index": 1,
"fee_sats": 700,
"fee_rate_sat_vb": 5.0,
"vbytes": 140,
"rbf_signaling": true,
"locktime": 850000,
"locktime_type": "block_height",
"psbt_base64": "cHNidP8BAFICAAAA...",
"warnings": [{ "code": "RBF_SIGNALING" }]
}4 test suites covering:
| Suite | What it tests |
|---|---|
coin-selection.test.ts |
Selection algorithm, insufficient funds, max_inputs policy |
weight.test.ts |
vbytes estimation for all script types |
rbf-locktime.test.ts |
All combinations of rbf/locktime/current_height |
validate.test.ts |
Input validation, error handling, edge cases |
| Layer | Tech |
|---|---|
| Language | TypeScript |
| Runtime | Node.js |
| Server | Express |
| Bitcoin | bitcoinjs-lib, ecpair, tiny-secp256k1 |
| Testing | Jest + ts-jest |
| Frontend | Vanilla HTML/CSS/JS |
This project taught me how Bitcoin wallets actually work under the hood β how coin selection isn't just "pick UTXOs until you have enough" but involves carefully balancing fees, change outputs, and dust rules. The fee/change circular dependency (adding change changes the size, which changes the fee, which might eliminate the change) was surprisingly tricky to get right.
Building the PSBT from scratch also gave me a deeper understanding of BIP-174 and why the signing workflow is split into creation β signing β finalization steps.
MIT β see LICENSE.
Built as part of the Summer of Bitcoin 2026 developer challenge.
