Data access · 10 min read

Internal transactions explained: traces and the cost of getting them

Most of what happens on an EVM chain happens inside transactions, not as transactions. A swap routed through three contracts is one submitted transaction and a tree of internal calls underneath it. Those internal calls are what block explorers label internal transactions, and they are missing from the two data types most teams start with. This guide explains what they are, why a transaction list and event logs both miss them, what a trace record holds, and why traces are the one data type that is genuinely hard to get from a node. Every figure is from a real query against SQD's Portal over a fixed block range.

Updated 2026-06-15 · By the SQD team

1. What an internal transaction is

The name is a small lie that stuck. An internal transaction is not a transaction. Only the top-level call, the one an account signs and pays gas for, is a real transaction. Everything that call triggers as it runs, one contract calling another, a contract forwarding ETH, a proxy delegating to its implementation, is an internal call. Block explorers group these under an "Internal Txns" tab because that is how they read to a person, but in the data they are execution traces.

A single transaction can produce dozens of traces. A user calls a router; the router calls a pool; the pool calls a token contract; the token moves a balance; the pool sends ETH back. That is one transaction and five or more internal calls, arranged as a tree. The tree is where the real behavior of a transaction lives, which is why accounting tools, DeFi analytics, and investigators all end up needing it. For the investigation angle specifically, following funds through internal calls for transaction monitoring, the compliance data guide goes deeper; this one is about the data type itself and what it costs to obtain.

2. Why the transaction list misses them

The two data types most teams reach for first both leave internal calls out, for different reasons. The transaction list contains only top-level calls by definition, so anything inside a transaction is gone. Event logs exist only when a contract's code chooses to emit one, and most internal calls, along with every plain ETH forward, emit nothing.

The size of the gap is easy to measure. Take USDC on Ethereum over 20 blocks (21,000,000 to 21,000,020) and count it two ways. As the direct recipient of a transaction, the contract a user sent their transaction to, USDC appears 66 times. As the callee of a call, once internal calls are included, it appears 260 times. The 194-call difference is USDC being called from inside transactions aimed at something else: a DEX router, an aggregator, a multisig. A query that filters transactions by their to field cannot see any of it.

USDC on Ethereum, blocks 21,000,000-21,000,020

transactions where to = USDC ............  66
calls where callTo = USDC ...............  260
                                          ----
internal calls a tx filter misses .......  194

That ratio is not special to USDC. Any contract that other contracts call, which is most of the useful ones, is touched far more often from inside transactions than as a direct recipient. To see those touches you have to query the call tree itself.

3. The trace record

SQD's Portal exposes traces as a first-class data type. The query that finds every internal call into USDC, the 260 above, filters the trace stream by call type and callee:

POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream
Accept: application/x-ndjson

{
  "type": "evm",
  "fromBlock": 21000000,
  "toBlock": 21000020,
  "traces": [{
    "type": ["call"],
    "callTo": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]
  }],
  "fields": {
    "trace": {
      "type": true,
      "traceAddress": true,
      "callFrom": true,
      "callTo": true,
      "callValue": true,
      "callCallType": true,
      "error": true
    }
  }
}

Each trace carries its position in the call tree. traceAddress is [] for the top-level call, [0] for the first internal call, [0,0] for a call made inside that one, and so on, so the whole tree is reconstructable:

Transaction                              traceAddress []
|- call  [0]      router -> pool          callValue 0
|  |- call [0,0]  pool   -> token         callValue 0
|  \- call [0,1]  pool   -> recipient     callValue X  (ETH out)
\- call  [1]      router -> fee sink      callValue Y

Two details to know before building on this. The response nests its data: the field requested as callTo arrives under action.to, callFrom under action.from, and the call type under action.callType, which separates an ordinary call from a delegatecall or a read-only staticcall. And traces carry a transactionIndex, not a transaction hash, so to tie a hop back to a hash you include the transaction in the query. With that, callValue on each hop is the ETH moved there, and the path of execution through a transaction is laid out rather than guessed.

4. Why traces cost so much from RPC

Logs and transactions are stored by every node as a normal part of keeping the chain, so reading them back is cheap. Traces are not stored. To produce them a node has to re-execute the transaction through an instrumented EVM that records every call as it goes. That is why debug_traceTransaction and trace_block are the heaviest methods a node exposes.

The practical consequences follow from that cost. Public and shared RPC endpoints commonly disable the tracing methods outright or rate-limit them hard, so they are often simply not available on the endpoint you have. Getting traces for historical blocks means running your own archive node with tracing enabled, which is the most demanding node configuration to operate. And because each call re-executes a transaction, pulling traces across a long range one transaction at a time is slow even when it is permitted. The result is that traces are usually the data type a project does not have, not because the queries are hard to write but because the source is hard to reach.

A read-side data layer changes the position. SQD does the re-execution and decoding once and serves the result as a queryable type, so the 20-block trace query above runs against an indexed stream rather than against a node's debug endpoint, and the same query works over a single block or the full history. The work that makes traces expensive does not go away; it stops being something every consumer has to pay for.

5. A second use: contract deployments

Following calls is the obvious use of traces, but the same data answers a different question cleanly: who deployed which contract, and when. A deployment is a create trace, a separate trace type, and filtering for it returns the deployer and the new contract address. This query scans a 10-block range for deployments:

{
  "type": "evm",
  "fromBlock": 21000000,
  "toBlock": 21000010,
  "traces": [{ "type": ["create"] }],
  "fields": {
    "block": { "number": true },
    "trace": { "type": true, "createFrom": true, "createResultAddress": true, "transactionIndex": true }
  }
}

One real result, the first deployment in block 21,000,000, with the response shown in its nested form:

{
  "type": "create",
  "transactionIndex": 14,
  "action": { "from": "0xcdc180b5dd8ede4dfbe212f2aefc8789d16089b8" },
  "result": { "address": "0x521de88737bda991043ee9af429ec1fcabb87f23" }
}

The deployer is action.from and the contract it created is result.address. This is the definitive way to build a deployment registry, because it captures contracts made by factory contracts and by CREATE2, both of which look like ordinary calls in a transaction list rather than deployments. For DeFi work that means catching a new pool the moment its factory deploys it, from the trace stream, without watching for a hand-maintained address list.

6. Traces with SQD

The queries above are the actual interface. SQD provides traces as decoded, typed records from the Portal, alongside logs and transactions, so the internal calls a log-and-transaction feed misses are in scope rather than out of reach. The same query shape runs against every EVM dataset SQD indexes, changing only the network name in the endpoint, with full history from genesis.

For a production pipeline you would stream traces rather than hand-write requests. The Squid and Pipes SDKs pull traces into your own store with the same call and create filters, where they become internal-transfer tables, swap-path records, or a deployment registry that updates as blocks arrive. For following funds through traces in a monitoring context, see the compliance data guide; for how trace-level data feeds trading and DeFi products, see the DeFi and trading solution page.

Frequently asked questions

What is an internal transaction?
An internal transaction is not a transaction at all. It is a call one contract makes to another while a single submitted transaction executes. Only the top-level call is a real transaction signed and paid for by an account; everything it triggers downstream, a router calling a pool, a pool calling a token, a contract forwarding ETH, is an internal call. Block explorers label these "internal transactions" because that is how they look to a user, but in the data they are execution traces.
Why do internal transactions not show up in the logs or the transaction list?
The transaction list only contains top-level calls, so anything that happens inside a transaction is absent. Event logs only exist when a contract chooses to emit one, and most internal calls and plain ETH forwards emit nothing. Over a sample of 20 Ethereum blocks, USDC was the direct recipient of 66 transactions but was called 260 times once internal calls are counted; the 194-call difference happened inside transactions aimed at other contracts and is invisible to a transaction filter.
How do I query internal transactions (traces)?
Internal transactions live in the traces data type. With SQD's Portal you query traces and filter by callTo or callFrom for an address, or by callSighash for a function; each match returns the call with a traceAddress placing it in the call tree, a callValue for any ETH moved, and the call type. Response fields nest under action and result objects, so callTo arrives as action.to and a deployed contract address as result.address.
Why are traces expensive to get from an RPC node?
Producing traces means re-executing transactions with an instrumented EVM, which is why debug_traceTransaction and trace_block are the heaviest JSON-RPC methods. Public and shared endpoints commonly disable or tightly rate-limit them, and getting historical traces requires running your own archive node with tracing enabled. Pulling traces across a long range one transaction at a time is slow and often simply unavailable, which is why traces are usually the missing data type rather than a hard one.
Can I find which contract a deployer created from traces?
Yes. Contract deployments are CREATE traces. Filter traces by type "create" and the deployer address (createFrom), and each result gives the deployer as action.from and the new contract as result.address. This is the definitive source for a deployment registry, since it captures contracts created by factories and by CREATE2, which a transaction list does not surface as plain deployments.

Need internal calls and deployments at scale?

See how trace-level data feeds DeFi and trading products on the DeFi and trading solution page.