UTXO and account models · Lesson 1 of 6
The UTXO model for analysts
8 min read
Bitcoin and the chains derived from it (Litecoin, Bitcoin Cash, Dogecoin and others) store no balances. They store discrete outputs that are created by one transaction and destroyed by another. Every balance, flow, trace and cluster an analyst produces for these chains is built on that structure, and most beginner errors (double counting, phantom balances, "payments" that were really change) come from forgetting it.
Coins are outputs
A transaction output is an amount plus a locking script (the scriptPubKey). It is identified by its outpoint: the txid of the transaction that created it and its position in that transaction's output list, written txid:vout. An output has only two states: unspent or spent, and it is spent exactly once and in full. There is no partial spending and no "account" to draw from.
An input does not carry an amount. It points to an earlier outpoint and supplies the data (signature, public key, script) that satisfies that output's locking script. To know what an input is worth, you look up the output it spends (see the lesson Raw Bitcoin transactions).
Three consequences follow for analysis:
| What users see | What the chain records |
|---|---|
| "My address holds 1.2 BTC" | Several unspent outputs whose scripts decode to that address, summing to 1.2 BTC |
| "I sent 0.5 BTC" | A transaction that consumed whole outputs and created new ones, usually including change |
| "My balance went down" | Outputs moved from the unspent set to the spent set |
An address balance is therefore a derived figure, never a stored one. New coins enter through the coinbase transaction, which has no real inputs and pays the block subsidy plus fees. Coinbase outputs can be spent only in a block at least 100 blocks after the one that created them, a rule called coinbase maturity.
The spending graph
Because every input points to exactly one earlier output, the chain forms a spending graph. Here is a small decoded example:
{
"txid": "b7e0…5d19",
"vin": [
{ "txid": "4f3a…c21e", "vout": 0 },
{ "txid": "9b1d…07aa", "vout": 1 }
],
"vout": [
{ "n": 0, "value": 1.00000000, "address": "3J9x…Qe1m" },
{ "n": 1, "value": 0.09950000, "address": "bc1q…r8tn" }
]
}
Transaction b7e0…5d19 spends output 0 of 4f3a…c21e (0.80 BTC) and output 1 of 9b1d…07aa (0.30 BTC). Inputs total 1.10 BTC; outputs total 1.0995 BTC; the fee is the difference, 0.0005 BTC. The two outputs are now unspent and will become edges only when something spends them.
Read as a graph, transactions are nodes and outputs are directed edges from the transaction that created them to the transaction that spent them. Unspent outputs are edges with no destination yet. The graph is acyclic and always points forward in time, so any satoshi can be followed backward, hop by hop, to the coinbase transactions that created it.
One property matters more than any other: inside a transaction, the protocol does not link specific inputs to specific outputs. When 0.80 and 0.30 BTC go in and 1.00 and 0.0995 come out, nothing on chain says which input "paid" which output. Value is pooled and redistributed. Every tracing method that assigns inputs to outputs (first-in-first-out, haircut and so on) is an analyst's convention, not a fact read from the data; the lesson Tracing funds forward and backward compares them.
Change outputs
Since outputs are spent whole, a payment usually sends part of the value to the recipient and returns the rest to the sender as change. In the example above, one of the two outputs is probably change, but the protocol does not mark which. Identifying it is a matter of heuristics (script type, round amounts, address freshness), covered in the lesson Change detection heuristics.
Getting change wrong has large effects. Treat change as a payment and you invent a counterparty and send the trace down the wrong branch. Count all outputs as transferred value and on-chain "volume" is inflated, because change and self-transfers move no value between parties. Serious volume measures try to exclude change and consolidations, and every such measure is only as good as its change model.
Recognizing a few common transaction shapes helps:
| Shape | Typical structure | Usual meaning |
|---|---|---|
| Simple payment | 1–3 inputs, 2 outputs | Payment plus change |
| Consolidation | Many inputs, 1 output | One wallet merging small outputs, often when fees are low |
| Batch payout | Few inputs, many outputs | A service paying many customers at once |
| Peel step | 1 input, 2 outputs: one small, one large | Part of a peel chain; the large output continues (see the lesson Graph metrics and patterns) |
Coin age and dormancy
Every output records the block in which it was created, so every unspent output has an age. When an output is spent, analysts compute coin days destroyed (CDD): the amount multiplied by the number of days it sat unspent. An output of 2 BTC created 400 days earlier destroys 800 coin days when spent. A day's total CDD divided by the BTC moved that day gives an average dormancy: how long, on average, the coins that moved had been sitting still.
Age matters in investigations because long-dormant coins that suddenly move are a classic alert. Much of the bitcoin stolen from Bitfinex in 2016 stayed in the hacker's wallet for years until authorities seized about 94,000 BTC in 2022. Monitoring outputs linked to an old incident for their first spend is one of the simplest and most valuable rules an analyst can set.
A breakdown of the UTXO set by age, at a fixed height, looks like this in SQL. The schema is a typical one: an outputs table with one row per output, and an inputs table in which each row names the outpoint it spends.
-- Unspent outputs at height 900000, grouped by age (about 144 blocks per day)
WITH spent AS (
SELECT prev_txid, prev_vout
FROM inputs
WHERE block_height <= 900000
)
SELECT
CASE WHEN 900000 - o.block_height < 144 * 30 THEN '1: under 1 month'
WHEN 900000 - o.block_height < 144 * 365 THEN '2: 1-12 months'
ELSE '3: over 1 year' END AS age_band,
COUNT(*) AS utxos,
SUM(o.value_sat) / 100000000.0 AS btc
FROM outputs o
LEFT JOIN spent s
ON s.prev_txid = o.txid AND s.prev_vout = o.vout
WHERE o.block_height <= 900000
AND s.prev_txid IS NULL -- no spend at or before the height
GROUP BY 1
ORDER BY 1;
The query keeps only outputs created at or before the chosen height that have no matching spend at or before that height, which is the definition of the UTXO set at that point. The age bands use block counts as a proxy for time; for precise ages, join to the blocks table and use block timestamps (bearing in mind the timestamp caveats in the lesson Blocks and headers as data). Note the 100000000.0 divisor: integer division would silently truncate in some engines.
The UTXO set
The set of all unspent outputs at a given block is the UTXO set. Nodes keep it in a dedicated database (the chainstate) because it is what they need to validate new transactions. It has grown large, pushed up in part by inscription and token experiments that created many small outputs, so queries over it are heavy.
Two Bitcoin Core RPC methods are useful here:
gettxoutsetinforeturns summary statistics for the set, includingtxouts(the number of unspent outputs) andtotal_amount(the coins they hold). Asking for a historical height requires the optional coinstatsindex.scantxoutsetsearches the current UTXO set for outputs matching output descriptors such asaddr(...)and returns each match with its txid, vout, amount and height. It sees only unspent outputs, never history.
Bitcoin Core keeps no address index, so a full transaction history for an address requires an indexer or a data warehouse (see the lesson Node APIs and data access).
The set also gives a sanity check. Its total_amount should be close to, but below, the total subsidy ever issued, because some value is provably unspendable and never enters the set: the genesis block reward, OP_RETURN outputs, and fees or subsidy that miners failed to claim. A pipeline whose computed UTXO total exceeds issuance has a bug, typically missing spends or duplicated outputs.
Address reuse
Addresses are not accounts. Well-behaved wallets derive a fresh address for every receipt from a hierarchical deterministic seed, and the user never sees most of them. One person can control thousands of addresses; one exchange hot-wallet address can touch millions of counterparties.
Address reuse (receiving several times at the same address) is common anyway: donation addresses, some exchange deposit addresses, old wallets and careless users. For an analyst it is a gift and a trap. Reuse links payments together without any heuristic, and once an address has spent, its public key is visible on chain. But address-level metrics such as "active addresses" or "addresses holding more than 1 BTC" measure scripts, not people, and should never be presented as user counts.
One subtlety: the same public key can produce several different addresses, one per script type (for example legacy P2PKH and native SegWit P2WPKH). They look unrelated, but once the key is revealed in a spend, an analyst can link them by the public key itself.
Exam focus: an output is identified by its outpoint (txid:vout) and is spent once and in full; inputs carry no amount, so input values and balances are derived from the outputs themselves; the protocol does not link specific inputs to specific outputs, so input-to-output tracing is a convention; change outputs and why misclassifying them distorts traces and volume; coinbase maturity of 100 blocks, and coin days destroyed (amount × days unspent) and dormancy as alerts; computing the UTXO set at a height (created at or before, not spent at or before);gettxoutsetinfoandscantxoutsetand their limits; address reuse and why address counts are not user counts.