Skip to content

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.

Terminal window
# A throwaway wallet and an address to fund
bitcoin-cli -regtest createwallet "lab"
addr=$(bitcoin-cli -regtest getnewaddress)
# Mine 101 blocks to that address: coinbase needs 100 confirmations to mature
bitcoin-cli -regtest generatetoaddress 101 "$addr"
# Find a UTXO we can spend
bitcoin-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.

Terminal window
dest=$(bitcoin-cli -regtest getnewaddress) # pretend recipient
change=$(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...00000000

That 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.

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:

Terminal window
# Just the payment; let the node pick inputs + add change + fee
funded=$(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.

Terminal window
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.

Terminal window
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.

Terminal window
txid=$(bitcoin-cli -regtest sendrawtransaction "$signed")
echo "$txid"
7f8e0a1b2c3d4e5f60718293a4b5c6d7e8f9...c2

The node validated it (signatures, no double-spend, fee ≥ minimum) and put it in the mempool. On regtest, mine it into a block to confirm:

Terminal window
bitcoin-cli -regtest generatetoaddress 1 "$addr"
bitcoin-cli -regtest gettxout "$txid" 0 # now a confirmed UTXO

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.

  1. 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?
  2. What happens if you forget the change output entirely? Compute the fee for a one-output 10-BTC spend of a 50-BTC input.
  3. Why does the signed hex string get longer than the unsigned one?
  4. Run decoderawtransaction on your signed tx and verify sum(inputs) - sum(outputs) matches the fee fundrawtransaction reported.
  5. After sendrawtransaction, run getrawmempool. Is your txid there? Why isn’t it confirmed yet?