Greedy coin selection · 6 script types · Full-stack React visualizer
Coin Smith takes a UTXO set, a set of payment outputs, a change template, and a fee rate — and produces a fully valid, unsigned PSBT ready for signing. It handles coin selection, fee estimation, dust filtering, change construction, RBF signaling, and locktime semantics in a single deterministic pipeline. Everything is exposed through a structured JSON report and an interactive web UI.
The name fits: it forges raw UTXOs into a safe, spendable transaction.
| Metric | Value |
|---|---|
| Script types supported | 6 (p2wpkh, p2wsh, p2pkh, p2sh, p2tr, p2sh-p2wpkh) |
| Public fixtures | 26+ |
| Coin selection strategy | Greedy, largest-first |
| Dust threshold | 546 sats |
| Fee model | vbyte-accurate per script type |
| External runtime deps | bitcoinjs-lib, express |
Three layers — validation, selection, construction — with a REST API and React UI on top.
┌──────────────────────────────────────────────────────────────────┐
│ USER / BROWSER │
│ React + TypeScript + Tailwind + TanStack Query │
│ ┌──────────────────┐ ┌────────────────────────────────┐ │
│ │ JSON Input │ │ Result Viewer │ │
│ │ (fixture) │ → │ inputs · outputs · PSBT b64 │ │
│ └──────────────────┘ └────────────────────────────────┘ │
└──────────────────────────────┬───────────────────────────────────┘
│ HTTP/JSON (REST API)
┌──────────────────────────────▼───────────────────────────────────┐
│ Node.js + Express │
│ │
│ GET /api/health → liveness check │
│ POST /api/build → validate → select → build PSBT → respond │
│ │
└──────────────────────────────┬───────────────────────────────────┘
│ in-process function calls
┌──────────────────────────────▼───────────────────────────────────┐
│ TypeScript Core │
│ │
│ ① validate.ts Defensive fixture validation │
│ ② helperFunctions Coin selection (greedy) + fee/vbyte calc │
│ ③ psbtCode.ts PSBT construction via bitcoinjs-lib │
│ · RBF + locktime semantics (BIP-125, anti-fee-sniping) │
│ · witness_utxo / non_witness_utxo per input │
│ · change inclusion / dust filtering │
│ · warnings: HIGH_FEE · DUST_CHANGE · SEND_ALL · RBF_SIGNALING│
│ ④ main.ts CLI entry point → writes out/<name>.json │
└──────────────────────────────────────────────────────────────────┘
The selector uses a greedy, largest-first strategy:
- Sort UTXOs descending by value
- Accumulate inputs until
Σ inputs ≥ Σ payments + estimated_fee - Compute change; if change ≥ 546 sats → include it, else fold into fee and recalculate
- Enforce
policy.max_inputsif provided — raisePOLICY_VIOLATIONotherwise
Fee is calculated using a per-script-type vbyte model:
weight = 40 + 2 + (nonwitness_bytes × 4) + witness_bytes + (output_count × 4)
vbytes = ceil(weight / 4)
Nonwitness/witness sizes are looked up per script type (p2wpkh, p2tr, p2pkh, etc.).
rbf |
locktime present |
current_height |
nSequence |
nLockTime |
|---|---|---|---|---|
| false/absent | no | — | 0xFFFFFFFF |
0 |
| false/absent | yes | — | 0xFFFFFFFE |
locktime |
| true | no | yes | 0xFFFFFFFD |
current_height |
| true | yes | — | 0xFFFFFFFD |
locktime |
| true | no | no | 0xFFFFFFFD |
0 |
Locktime is classified in the JSON report:
"none"—nLockTime == 0"block_height"—0 < nLockTime < 500,000,000"unix_timestamp"—nLockTime ≥ 500,000,000
Coin Smith flags safety conditions in the output report:
| Code | Condition |
|---|---|
HIGH_FEE |
fee_sats > 1,000,000 or fee_rate_sat_vb > 200 |
DUST_CHANGE |
Change output exists but value_sats < 546 |
SEND_ALL |
No change output — all leftover consumed as fee |
RBF_SIGNALING |
Any input has nSequence ≤ 0xFFFFFFFD |
┌── TypeScript / Node.js ──────────────────────────────────────────┐
│ main.ts CLI entry point, fixture → JSON report │
│ psbtCode.ts PSBT construction (bitcoinjs-lib) │
│ helperFunctions Coin selection, fee calc, vbyte model │
│ validate.ts Defensive fixture validation │
│ server.ts Express REST API + static file serving │
└──────────────────────────────────────────────────────────────────┘
┌── React + TypeScript ────────────────────────────────────────────┐
│ Vite Build tooling │
│ Tailwind CSS Utility-first styling │
│ TanStack Query Async state management │
│ Lucide React Icons │
│ Components JsonInput · ResultDisplay · Output badges │
└──────────────────────────────────────────────────────────────────┘
┌── Testing ───────────────────────────────────────────────────────┐
│ Vitest 25+ unit tests │
│ Fixtures 26+ JSON test cases │
└──────────────────────────────────────────────────────────────────┘
coin-smith/
├── psbt/
│ ├── main.ts CLI entry point
│ ├── server.ts Express API + static serving
│ ├── psbtCode.ts PSBT construction logic
│ ├── helperFunctions.ts Coin selection + fee calculation
│ ├── validate.ts Input validation
│ ├── types.ts TypeScript interfaces
│ ├── coinsmith.test.ts Vitest unit tests
│ ├── package.json
│ ├── tsconfig.json
│ └── client/ React frontend (Vite + Tailwind)
│ ├── src/
│ │ ├── App.tsx
│ │ ├── pages/
│ │ │ └── Index.tsx
│ │ ├── components/
│ │ └── lib/api.ts
│ ├── vite.config.ts
│ └── package.json
├── fixtures/ 26+ JSON test fixtures
├── out/ Generated PSBT reports
├── cli.sh CLI entry point script
├── web.sh Web server entry point script
└── setup.sh Install deps + build
./setup.shInstalls Node.js dependencies and builds the TypeScript backend and React frontend.
./cli.sh fixtures/basic_change_p2wpkh.json
# Output written to out/basic_change_p2wpkh.json./web.sh
# → http://127.0.0.1:3000Paste any fixture JSON into the UI, hit Build, and inspect the selected inputs, constructed outputs, fee breakdown, RBF/locktime status, and PSBT base64.
{
"network": "mainnet",
"utxos": [
{
"txid": "11...",
"vout": 0,
"value_sats": 100000,
"script_pubkey_hex": "0014...",
"script_type": "p2wpkh",
"address": "bc1..."
}
],
"payments": [
{
"address": "bc1...",
"script_pubkey_hex": "0014...",
"script_type": "p2wpkh",
"value_sats": 70000
}
],
"change": {
"address": "bc1...",
"script_pubkey_hex": "0014...",
"script_type": "p2wpkh"
},
"fee_rate_sat_vb": 5,
"rbf": true,
"locktime": 850000,
"current_height": 850000,
"policy": { "max_inputs": 5 }
}script_pubkey_hex is authoritative — addresses are display-only.
On error:
{ "ok": false, "error": { "code": "INSUFFICIENT_FUNDS", "message": "..." } }- Greedy selection, not branch-and-bound. For wallet use the marginal improvement from optimal selection is small; greedy is fast, predictable, and auditable.
- Two-pass change logic. Adding a change output changes vbytes, which changes the required fee. Coin Smith recomputes after the first pass to handle boundary conditions correctly.
script_pubkey_hexover address. Addresses are ambiguous across networks and formats. The hex script is the authoritative spend condition used in PSBT construction.witness_utxoalways set. For segwit inputs,witness_utxois required for hardware wallet signing. For legacy inputs,non_witness_utxo(full previous tx) is included.- Structured errors everywhere. Both CLI and API return
{ ok, error: { code, message } }— never raw stack traces.
- BIP-174 — Partially Signed Bitcoin Transaction Format
- BIP-125 — Opt-in Full Replace-by-Fee Signaling
- BIP-141 — Segregated Witness
- BIP-341 — Taproot
- Bitcoin Wiki — Transaction, Script, UTXO model
Built by Saniddhya Dubey
{ "ok": true, "network": "mainnet", "strategy": "greedy", "selected_inputs": [ { "txid": "...", "vout": 0, "value_sats": 100000, "script_pubkey_hex": "...", "script_type": "p2wpkh", "address": "bc1..." } ], "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" }] }