Build & Decode a Raw Transaction
You’ve read transactions. Now you’ll build one from scratch, field by field, on a sandbox chain. This is where transaction anatomy stops being a diagram and becomes something you typed. The payoff: you’ll see with your own eyes that the fee is not a field you set — it’s whatever value you fail to spend.
Setup: get a spendable coin on regtest
Section titled “Setup: get a spendable coin on regtest”# A throwaway wallet and an address to fundbitcoin-cli -regtest createwallet "lab"addr=$(bitcoin-cli -regtest getnewaddress)
# Mine 101 blocks to that address: coinbase needs 100 confirmations to maturebitcoin-cli -regtest generatetoaddress 101 "$addr"
# Find a UTXO we can spendbitcoin-cli -regtest listunspent[ { "txid": "a1b2c3...f9", "vout": 0, "amount": 50.00000000, "confirmations": 101, "spendable": true }]We now hold a 50-BTC UTXO. The job: spend it to a recipient, return change to ourselves, and let the difference become the fee.
Step 1 — createrawtransaction: declare inputs and outputs
Section titled “Step 1 — createrawtransaction: declare inputs and outputs”A raw transaction is just which prior outputs I’m consuming and which new outputs I’m creating.
We reference our input by txid:vout and define two outputs: the payment and the change.
dest=$(bitcoin-cli -regtest getnewaddress) # pretend recipientchange=$(bitcoin-cli -regtest getnewaddress) # our change address
# Inputs: 50 BTC. Outputs: 10 to dest, 39.9999 back as change.# The missing 0.0001 BTC will become the fee. WE choose this gap deliberately.raw=$(bitcoin-cli -regtest createrawtransaction \ '[{"txid":"a1b2c3...f9","vout":0}]' \ '[{"'$dest'":10.0},{"'$change'":39.9999}]')echo "$raw"0200000001f9...c3b2a10000000000ffffffff0200ca9a3b00000000160014...00000000That hex is a complete, unsigned transaction. Note what createrawtransaction does not do: it
never checks that your inputs cover your outputs, and it has no fee field. The fee is implicit.
The easier path: fundrawtransaction
Section titled “The easier path: fundrawtransaction”Manually balancing inputs, change, and fee is error-prone. Bitcoin Core can do it for you: give it only the outputs you care about and it selects inputs, adds a change output, and picks a fee:
# Just the payment; let the node pick inputs + add change + feefunded=$(bitcoin-cli -regtest createrawtransaction '[]' '[{"'$dest'":10.0}]')bitcoin-cli -regtest fundrawtransaction "$funded"{ "hex": "0200000001...00000000", "changepos": 1, "fee": 0.00000141}It even tells you the fee it chose and where it put the changepos. Use the manual path to
understand; use fundrawtransaction to be safe in practice.
Step 2 — signrawtransactionwithwallet: prove ownership
Section titled “Step 2 — signrawtransactionwithwallet: prove ownership”An unsigned transaction is a proposal. To make it valid, each input needs a signature proving you control the coin it spends — the digital-signature half of how Bitcoin enforces ownership without a custodian.
signed=$(bitcoin-cli -regtest signrawtransactionwithwallet "$raw" | jq -r '.hex'){ "hex": "0200000001f9...4830450221...012103abcd...ffffffff02...00000000", "complete": true}complete: true means every input is now signed and the transaction is ready. The hex grew — that’s
the scriptSig/witness data carrying your signatures.
Step 3 — decoderawtransaction: read it back before you send
Section titled “Step 3 — decoderawtransaction: read it back before you send”Always decode and eyeball a transaction before broadcasting. This is your last chance to catch a mistake.
bitcoin-cli -regtest decoderawtransaction "$signed"{ "txid": "7f8e...c2", "vsize": 141, "vin": [ { "txid": "a1b2c3...f9", "vout": 0, "sequence": 4294967295 } ], "vout": [ { "value": 10.00000000, "n": 0, "scriptPubKey": { "address": "<dest>" } }, { "value": 39.99990000, "n": 1, "scriptPubKey": { "address": "<change>" } } ]}Map it back to transaction anatomy: one vin (our 50 BTC),
two vout (payment + change). Now compute the fee by hand — conservation of value says
fee = sum(inputs) - sum(outputs):
inputs: 50.00000000 BTC outputs: 10.00000000 + 39.99990000 = 49.99990000 BTC ------------------------------------------------ fee = 50.00000000 - 49.99990000 = 0.00010000 BTC (10,000 sats)That 0.0001 BTC is exactly the gap we left in step 1. The fee was never typed anywhere — it’s the difference, claimable by whichever miner includes the transaction. See fees, change & conservation for the full law.
Step 4 — sendrawtransaction: broadcast
Section titled “Step 4 — sendrawtransaction: broadcast”txid=$(bitcoin-cli -regtest sendrawtransaction "$signed")echo "$txid"7f8e0a1b2c3d4e5f60718293a4b5c6d7e8f9...c2The node validated it (signatures, no double-spend, fee ≥ minimum) and put it in the mempool. On regtest, mine it into a block to confirm:
bitcoin-cli -regtest generatetoaddress 1 "$addr"bitcoin-cli -regtest gettxout "$txid" 0 # now a confirmed UTXOThe thread
Section titled “The thread”You just performed the entire trust-minimized handshake by hand: you proved ownership with a signature, conserved value to the satoshi, and broadcast a self-contained fact that any stranger’s node can verify without asking you anything. The fee emerged naturally as the value you left on the table for whoever does the work of including you. No bank approved this — math did.
Check your understanding
Section titled “Check your understanding”- On regtest, build a transaction by hand that spends one 50-BTC UTXO and creates a single 49.99-BTC output. What is the fee, and why?
- What happens if you forget the change output entirely? Compute the fee for a one-output 10-BTC spend of a 50-BTC input.
- Why does the signed hex string get longer than the unsigned one?
- Run
decoderawtransactionon your signed tx and verifysum(inputs) - sum(outputs)matches the feefundrawtransactionreported. - After
sendrawtransaction, rungetrawmempool. Is your txid there? Why isn’t it confirmed yet?