Bitcoin has no balances. Its state is a set of unspent transaction outputs (UTXOs), and "your balance" is whatever your wallet adds up from the outputs it can spend. Ethereum stores a balance and a nonce for every account directly in its world state. Almost every difference in how you build, sign, and debug transactions on the two chains comes from this one design choice.
The UTXO model (Bitcoin)
A Bitcoin transaction consumes existing outputs as inputs and creates new outputs. Each output has an amount and a locking script, for example "spendable by whoever signs for this public key hash". The rules:
- An output is spent whole. To pay 0.3 BTC from a 1 BTC output, you create two outputs: 0.3 to the recipient and about 0.7 back to yourself as change.
- The fee is implicit:
sum(inputs) - sum(outputs). Leave out the change output by mistake and the whole difference goes to the miner. - An output can be spent only once. Two transactions that spend the same output conflict, and at most one can confirm.
Tx A: in: [UTXO#1 1.0 BTC]
out: [0.3 BTC -> Bob] [0.6999 BTC -> Alice change]
fee: 0.0001 BTC (implicit)
To validate new transactions, a node only needs the UTXO set, which it keeps in its chainstate database. There is no per-address balance table. Ask your node what a wallet can spend:
bitcoin-cli -rpcwallet=mywallet listunspent 1
bitcoin-cli -rpcwallet=mywallet getbalances
Every balance you see in a Bitcoin wallet is a sum computed on the fly.
The account model (Ethereum)
Ethereum's state maps each address to an account with four fields: nonce, balance, storageRoot and codeHash. A transfer just subtracts from one balance and adds to another. There is no change output and no coin selection.
Instead, every transaction from an externally owned account carries a nonce, which must equal the sender's current transaction count. Nonces give Ethereum replay protection and a strict ordering per sender.
import { JsonRpcProvider, formatEther } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const addr = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const balance = await provider.getBalance(addr); // wei, as bigint
const nextNonce = await provider.getTransactionCount(addr, "pending");
console.log(formatEther(balance), nextNonce);
Fees are explicit and paid from the sender's balance: gasUsed × effectiveGasPrice. The value sent is separate from the fee.
Token balances such as ERC-20s are not account balances. They are entries in the token contract's storage, so you read them with balanceOf on the contract, not with getBalance.
Side-by-side
| UTXO (Bitcoin) | Account (Ethereum) | |
|---|---|---|
| State | Set of unspent outputs | Map of address to account |
| Balance | Sum of spendable outputs, computed by the wallet | Stored field |
| Replay protection | Each output can be spent once | Per-account nonce |
| Fee | Inputs minus outputs (implicit) | gasUsed × price, from balance |
| Tx size / fee driver | Number of inputs and outputs | Computation and storage (gas) |
| Parallel validation | Natural: independent outputs don't conflict | Harder: transactions touching the same state must be ordered |
| Privacy | Fresh address per payment is the norm | One long-lived address is the norm |
| Programmability | Limited scripts | General smart contracts |
What this means when you write code
On Bitcoin
- Coin selection matters. Fees scale with transaction size in virtual bytes, not with the amount sent. A wallet holding hundreds of tiny UTXOs pays more to spend them, and outputs below the dust threshold can cost more to spend than they are worth. Consolidate when fees are low.
- Always create change. Use the wallet's funding RPCs (
send,walletcreatefundedpsbt) rather than building raw transactions by hand unless you really need to. - Unconfirmed chains are possible. You can spend an unconfirmed output. This is what makes child-pays-for-parent work, but it also means a child fails if its parent is replaced.
- Watch outputs, not addresses. Index by
txid:voutwhen tracking deposits.
On Ethereum and other account chains
- Nonce management is your problem. A transaction with nonce 7 cannot be mined until nonce 6 is. One stuck, underpriced transaction blocks everything after it from that account. Services that send in parallel need a single nonce allocator per key, or several keys.
- Replacement is by nonce. To cancel or speed up a transaction, send a new one with the same nonce and a higher fee.
- Reads are simple.
getBalance,balanceOf, done. Historical balances need an archive node or an indexer. - Contract state is shared. Two users calling the same contract touch the same storage, which is where front-running and ordering problems come from.
Other chains mix the two
- Cardano uses an extended UTXO model. Outputs can carry datums and be locked by validator scripts, which makes transaction outcomes predictable before submission.
- Solana uses accounts. Each transaction declares upfront which accounts it reads and writes, so the runtime can execute non-overlapping transactions in parallel.
- Sui tracks state as versioned objects with owners, which borrows ideas from UTXOs within an account-like programming model.
Summary
- UTXO chains track spendable outputs. Balances are computed, fees are implicit, and change outputs are mandatory.
- Account chains store balances and nonces. Fees are explicit, and ordering per sender comes from the nonce.
- On Bitcoin, get coin selection, change and dust handling right, and track deposits by outpoint.
- On Ethereum, manage nonces centrally, replace stuck transactions by nonce, and read token balances from the token contract.
Get the weekly commit
New blockchain deep dives every week.