Data access · 9 min read

Following Bitcoin money through UTXOs: one declarative query

Bitcoin does not have accounts or contracts. Value lives in unspent outputs, and it moves when a transaction spends some outputs as inputs and creates new ones. Tracing where money went is therefore a matter of following outputs into the inputs of later transactions. SQD's Portal serves Bitcoin inputs and outputs as first-class data, in the same declarative request shape it uses for EVM logs and Solana instructions, so you do this without running a full node or writing a parser. The scope is flow evidence, who paid whom, not reconstructed address balances. Every transaction below is real, from block 954,236.

Updated 2026-06-18 · By the SQD team

1. Flow evidence, not balances

One boundary up front, because it is the honest thing to state and it shapes everything below. This data tells you about movement: which address sent value into a transaction and which addresses it went to. It does not give you an address's current balance. A balance would mean summing every unspent output an address controls across all of history, which is a different and heavier reconstruction. What follows traces flows, not holdings.

That is still most of what fund tracing needs. Following value from one address to the next, transaction by transaction, is the core of Bitcoin forensics, and it is exactly what inputs and outputs give you. For the investigation workflow that chains these hops together, see compliance data for crypto.

2. Inputs and outputs in one shape

The request is the same declarative shape as every other chain, with Bitcoin's tables instead of logs: a window, the inputs and outputs tables, and a field selection. Bitcoin uses Bitcoin Core's own field names, so an input's sender is prevoutScriptPubKeyAddress (the address of the output being spent) and an output's recipient is scriptPubKeyAddress. This is the actual request, as a curl you can paste:

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/bitcoin-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "bitcoin", "fromBlock": 954236, "toBlock": 954236,
"transactions": [{}], "inputs": [{}], "outputs": [{}],
"fields": {
"transaction": { "transactionIndex": true, "txid": true },
"input": { "transactionIndex": true, "prevoutScriptPubKeyAddress": true, "prevoutValue": true },
"output": { "transactionIndex": true, "scriptPubKeyAddress": true, "value": true, "scriptPubKeyType": true }
}
}'

The response is newline-delimited JSON, one block per line, with the matching inputs and outputs attached. A single real output from that stream:

live response · portal.sqd.dev
{ "transactionIndex": 3,
"scriptPubKeyAddress": "bc1q6jd3mvuy7wgdy402t3wg3k6cs62dteunc80w5q",
"value": 0.00741561,
"scriptPubKeyType": "witness_v0_keyhash" }

Each input carries prevoutScriptPubKeyAddress and prevoutValue (the address and amount of the output it spends); each output carries scriptPubKeyAddress, value in BTC, and scriptPubKeyType. Because a block returns inputs and outputs as flat arrays, every row also carries a transactionIndex: group inputs and outputs by it, and join to the txid in the transactions table, to rebuild each transaction's senders and recipients. The one input without these is the coinbase (transactionIndex 0): it mints new value rather than spending a prior output, so its prevoutScriptPubKeyAddress and prevoutValue come back null, and flow logic should skip that row. The Portal's Bitcoin MCP tools normalize these to sender, recipient, and type for an agent, and the Pipes SDK has a Bitcoin source that streams the identical query into your own database for a standing flow table.

3. Reading a payment and its change

A single real transaction shows the basic flow. One input funds two outputs: a payment to one address, and change back to the sending address. The change is recognizable because it returns to the exact address that funded the input.

One input, a payment and its change
Input
0.01640293 BTC
Output · payment
0.00741561 BTC
Output · change (back to sender)
0.00895269 BTC
Real transaction a89a5662…3f3d30 in block 954,236, pulled from the stream above. The 0.00003463 BTC unaccounted for in the outputs is the miner fee. The change output returns to bc1qxp3g…kl6346, the exact address that funded the input, which is the clearest change signal.

4. Consolidation: many inputs, one output

The payment shape above is one of a handful that recur in flow analysis, and they all fall straight out of reading inputs against outputs. A consolidation spends several outputs controlled by one address into a single output, the pattern a wallet or exchange uses to sweep small UTXOs together. A real one from the same block, transaction 81acc1c4…a66635:

RoleAmount
  • 3 inputs, one address0.02813590 BTC
  • 1 output, pubkeyhash (legacy)0.02812774 BTC
  • Fee0.00000816 BTC
Consolidation · block 954,236

Three inputs, all from bc1q4f5u…xfpe5l, collapse into one legacy output, with 0.00000816 BTC left as fee. Reading the same inputs and outputs the other way, several outputs from one input, is a fan-out; the most informative non-payment shape, an output that carries data instead of value, is the subject of the next section.

5. Output types, including OP_RETURN

Every output carries a script type, which tells you the address format and, in one case, that the output carries data rather than value. Block 954,236 alone contains legacy, P2SH, SegWit, and Taproot outputs:

  • pubkeyhash 1... Legacy P2PKH
  • scripthash 3... P2SH
  • witness_v0_keyhash bc1q... SegWit v0
  • witness_v1_taproot bc1p... Taproot
  • nulldata OP_RETURN Data, value 0
The script type is on every output. nulldata is the OP_RETURN case: a zero-value output that embeds data, used as a protocol marker, by Runes and similar metadata schemes, rather than to move coins.

A real OP_RETURN-bearing transaction in the same block makes it concrete. Transaction ee4f2a5b…06099f pays one Taproot output and attaches a single zero-value data marker, exactly as the stream returns them:

Output typeValue
  • witness_v1_taproot0.08811049 BTC
  • nulldata (OP_RETURN)0.00000000 BTC
OP_RETURN · block 954,236

The nulldata output moves no coins; its scriptPubKeyAddress comes back null and its value is zero. It is a marker a protocol writes onchain, and reading it is just another output type in the same query, which is what makes this dataset as useful for protocol activity as for plain payments.

6. Why this is harder elsewhere

Most indexing frameworks and onchain MCP servers are built around the EVM account-and-log model, or around a single chain's curated endpoint. Bitcoin's UTXO model does not fit that shape, so the usual way to get raw inputs and outputs is to run a full node and write your own parser.

The harder thing is exposing Bitcoin's inputs and outputs as a declarative dataset in the same request shape as EVM and Solana, so one query model spans an account chain, an instruction chain, and a UTXO chain. That cross-model uniformity is the part that takes work, and it is what lets a single agent or pipeline follow value across all three.

For the EVM equivalent of following value inside a transaction, see internal transactions explained. For the one-query-shape idea across machines, see one query shape, every VM, and for how landing many chains together works, multi-chain indexing.

Frequently asked questions

How do you trace Bitcoin fund flows without running a node?
Bitcoin value moves through transaction inputs and outputs, and SQD's Portal serves both as first-class data, in the same declarative request shape used for EVM and Solana. You query a block range, attach inputs and outputs, and each input carries the sending address and value while each output carries the receiving address, value, and script type. Following an output that becomes a later input is how you trace a flow, and none of it requires running a Bitcoin full node yourself.
Does this give me an address balance?
No, and that distinction is the honest scope. The data is flow evidence: which address sent value into a transaction and which addresses received it. A current balance for an address would require summing every unspent output it controls across all of history, which is a separate reconstruction. This guide stays on movement, who paid whom, not on balances.
What is the difference between an input and an output in a Bitcoin transaction?
An output is a chunk of value locked to a script (usually an address). An input spends a previous output. So a transaction consumes one or more existing outputs as inputs and creates new outputs. In the Portal data, an input carries the address that controlled the output being spent and its value; an output carries the destination address, the value, and the script type, such as pubkeyhash, witness_v0_keyhash, or witness_v1_taproot.
How do you tell the change output from the payment?
A typical spend produces a payment output to the recipient and a change output back to the sender, because an input must be spent in full. The clearest signal is an output that returns to an address also seen on the input side. In the block 954,236 transaction this guide traces, a single input of 0.01640293 BTC funds a 0.00741561 BTC payment to one address and returns 0.00895269 BTC as change to the exact address that funded the input, which is what marks it as change.
What is an OP_RETURN output?
OP_RETURN is a provably unspendable output used to embed a small amount of data rather than to move value. In the Portal data it appears as an output with script type nulldata and a value of zero. Protocols like Runes and various inscription schemes use it as a marker, so it is useful evidence of protocol activity even though it carries no coins.

Tracing funds or building payments tooling?

See how flow data feeds monitoring and payments on the compliance and wallets and payments pages.