Home / Solutions / Accounting & Tax
Accounting & Tax

Accounting and tax data: full transaction history, internal transfers included

A report built from the transaction list alone misses value that moves in internal calls: a contract paying out ETH appears in traces, not as a transaction. SQD serves transactions, decoded token transfers, and traces for any address across 225+ networks, with UTC block timestamps and fee fields on every row. Fiat prices and cost-basis math join downstream; the extraction itself is a pinned query anyone can re-run.

225+ Networks
Traces Internal value
Block time UTC timestamps
Genesis Full history
The problem

A complete ledger has to book the value that moves in internal calls, and the transaction list never shows it

A report assembled from an address's transaction list under-reports: a contract paying out ETH does it in an internal call that appears in traces, not as a transaction, and when the recipient is a plain wallet no event log records it either. The complete movement record is three streams filtered by one address: transactions, decoded token transfers, and internal value moves from traces. Any subset silently books zero for the movements it cannot see. SQD serves all three across 225+ networks, with UTC block timestamps and gas-fee fields on every row; fiat prices and cost-basis math join downstream.

How it works

The payout a transaction list never shows

On 2026-06-26 at 08:21:35 UTC, one Ethereum transaction paid 15 addresses at once: 0.09 ETH went into the Disperse batch-payment contract (0xd152...2150), which forwarded 0.006 ETH to each recipient inside the same transaction. The chain records one transaction, to Disperse. The 15 payouts are internal calls:

Internal payouts, one transaction received (ETH)
Real, from transaction 0xea9b…8b17 at block 25,400,576. The 15 internal calls of 0.006 ETH sum to the 0.09 ETH sent, exactly.

For each of those 15 addresses, the transaction list records nothing. Filter that block by transactions from or to the first recipient and the stream returns a bare header, zero rows:

transactions from or to the recipient · same block
{"header":{"number":25400576,"timestamp":1782462095}}

No event log records it either: an internal ETH transfer to a plain wallet emits nothing, and this block holds no log from the Disperse contract and none that references the recipient in its topics (both checked). A ledger built from transactions and logs alone books zero income for that address on that day. The 0.006 ETH arrived anyway.

The row that does record it is a trace. One filter on internal calls into the address:

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 25400576, "toBlock": 25400576,
"traces": [{ "type": ["call"], "callTo": ["0x7cf8db81ac99bb55336f1e034098741a9fb68a06"], "transaction": true }],
"fields": {
"block": { "number": true, "timestamp": true },
"trace": { "type": true, "traceAddress": true, "callFrom": true, "callTo": true, "callValue": true, "error": true },
"transaction": { "hash": true }
}
}'
the row it returns
{
"header": { "number": 25400576, "timestamp": 1782462095 },
"transactions": [{ "hash": "0xea9b5936e3314af278a4f7c25f097d7a1272a609fe545151a6bf7296e2778b17" }],
"traces": [{
"traceAddress": [0],
"type": "call",
"error": null,
"action": {
"from": "0xd152f549545093347a162dce210e7293f1452150",
"to": "0x7cf8db81ac99bb55336f1e034098741a9fb68a06",
"value": "0x1550f7dca70000"
}
}]
}

Three things to know before booking it: the amount nests under action.value as hex wei (0x1550f7dca70000 is 6,000,000,000,000,000 wei, 0.006 ETH); a trace has no transaction hash of its own, so "transaction": true in the filter is what puts 0xea9b... in the response; and error: null is what says the value actually moved.

Standard JSON-RPC has no address-keyed lookup, and internal calls only exist in the non-standard debug and trace namespaces, so recovering this one row from a node means re-executing blocks and scanning the output yourself, per chain. Here the same trace filter runs unchanged across 225+ networks. Why traces are expensive to pull anywhere else is covered in the internal transactions guide.

Cost-basis inputs

Amounts, UTC timestamps, and fees on the row; the math stays yours

A cost-basis engine needs three inputs per movement: how much, when, and what it cost to execute. The same transaction that funded the payout above carries all three, in one row:

FieldReturnedDecoded
  • timestamp17824620952026-06-26 08:21:35 UTC
  • value0x13fbe85edc900000.09 ETH
  • gasUsed0x490bc299,196 gas
  • effectiveGasPrice0x13a15da40.329342372 gwei
  • status1executed, not reverted
The same row, decoded field by field

The block timestamp is unix seconds, which is UTC by construction: 1782462095 decodes to 2026-06-26 08:21:35 UTC, the key a fiat price lookup joins on. The fee is one multiplication, gasUsed times effectiveGasPrice: 299,196 × 329,342,372 wei = 98,537,920,332,912 wei, about 0.0000985 ETH. Nothing else needs a node call.

the funding transaction, receipt fields included
{
"header": { "number": 25400576, "timestamp": 1782462095 },
"transactions": [{
"hash": "0xea9b5936e3314af278a4f7c25f097d7a1272a609fe545151a6bf7296e2778b17",
"from": "0x45773e140fe17c958bb4fdf1268196fa9819a9b7",
"to": "0xd152f549545093347a162dce210e7293f1452150",
"value": "0x13fbe85edc90000",
"gasUsed": "0x490bc",
"effectiveGasPrice": "0x13a15da4",
"status": 1
}]
}

Because the query is pinned to block 25,400,576, it returns exactly this row on every run: run it twice and diff the output, byte-identical. An extraction is a command an auditor can re-execute, not a snapshot they have to trust.

A reverted value never moved

Across blocks 25,400,570 to 25,400,590, the trace stream carries 9,634 value-bearing calls, and 6 of them failed with execution reverted, the largest attempting to move exactly 2.73 ETH. A reverted call's value stayed where it was: book only rows where error is null. The opposite holds for fees: a reverted transaction still pays its gas, so the fee is a real cost even when the transfer is not.

The onchain record, not the tax return

SQD serves amounts, counterparties, timestamps, and fees. Fiat prices, tax lots, and cost-basis math join downstream in your accounting tool, with one exception: the executed price of a DEX trade is itself onchain, in the pool's Swap event. There is no balance-at-block field either; a balance on a date folds from the transfer history up to that date, as shown in the wallet history guide.

Capabilities

Built for Accounting & Tax at scale

The complete movement record

Transactions, decoded ERC-20, ERC-721, and ERC-1155 transfers, and internal value moves from traces, all filtered by address in one query shape. A history assembled from all three is what balances fold from; any subset silently under-reports.

Internal transfers included

A contract paying out ETH does it in an internal call: the value appears in traces, not in the transaction list, and when the recipient is a plain wallet no event log exists at all. Filter traces by callTo and callFrom to capture it, and check the error field: a reverted call carries a value that never moved.

Cost-basis inputs on every row

Every block carries a unix timestamp in UTC, the key a fiat price lookup joins on. Every transaction carries gasUsed, effectiveGasPrice, and status, so the exact fee is one multiplication. Fiat prices and tax-lot logic are not served; those join downstream in your accounting engine.

Re-runnable extractions

A query pinned to a finalized block range returns the same rows on every run, so an extraction is a command an auditor can re-execute, not a snapshot they have to trust. Full history from genesis streams through the same query shape as chain head.

Why SQD

What you would build yourself

The transactions, transfers, and internal calls are all onchain; the work is in reading the ones a transaction list leaves out.

  • Internal transfers
    Yourself A ledger built from transactions and logs alone books zero income for a contract payout to a plain wallet, which emits no event log at all.
    With SQD One trace filter on callTo and callFrom returns the internal call that moved the value, with error null confirming it settled.
  • Trace access
    Yourself Internal calls live only in the non-standard debug and trace namespaces, so recovering one row from a node means re-executing blocks and scanning the output yourself, per chain.
    With SQD The same trace filter runs unchanged across 225+ networks.
  • Reverted calls
    Yourself Counting a call's value without checking whether it succeeded books transfers that never settled, since a reverted call still carries a value.
    With SQD Every trace carries an error field, so booking only rows where it is null excludes reverted value, while the gas fee still counts as a real cost.
  • Auditability
    Yourself An extraction pulled ad hoc from a node is a snapshot an auditor has to trust.
    With SQD A query pinned to a fixed block range returns byte-identical rows on every run, so the extraction is a command an auditor can re-execute.

See this on your own contracts

Tell us the chains and contracts you care about, and we will show you the query that returns them.

Use cases

What teams build with SQD

  • Transaction history exports for tax software
  • Cost-basis input feeds: amounts, timestamps, fees
  • Internal-transfer-aware ledgers and reconciliation
  • Auditor-reproducible extractions over pinned ranges
  • Treasury and fund accounting from decoded transfers
  • Realized trade prices read from DEX swap events

Frequently asked questions

Is there an API for crypto accounting and tax data?
SQD's Portal serves the onchain record accounting tools consume: transactions, decoded token transfers, and internal value movements (traces) for any address, across 225+ networks, streamed as NDJSON with UTC block timestamps and gas-fee fields on every row. It is a data layer, not a tax calculator: fiat prices and cost-basis rules join downstream in your accounting engine. The queries that assemble a complete history are walked through in the wallet history guide.
How do I export the full transaction history of an address?
Three filters cover it: transactions where the address is sender or recipient, Transfer logs where it appears in the topics, and traces where it is callFrom or callTo, which is where internal transfers live. Each streams over a pinned block range, from genesis to chain head, as NDJSON, so the export lands in your own store (PostgreSQL, ClickHouse, Parquet) via the Squid or Pipes SDK. Re-running the same pinned range returns the same rows, so an auditor can reproduce the extraction from the query alone.
Does SQD calculate cost basis or capital gains?
No. SQD serves amounts, counterparties, UTC timestamps, and fees from the chain; fiat prices and tax-lot logic (FIFO, HIFO, specific identification) live in your accounting engine. One exception worth knowing: the executed price of a DEX trade is itself onchain, readable from the pool's Swap event, so realized trade prices can come from the same feed. Balances are not served as a field either; a balance on a date folds from the transfer history up to that date.
Get started

Your blockchain data infrastructure, handled.

Private Portal. Dedicated. Validated. Managed. Tell us what you're building, we'll show you what it looks like on SQD.