For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content

Decoding Transaction Traces

Traces are the calls a transaction makes once it starts running. They are how you see internal ETH transfers, calls that emit no event, and calls that reverted. None of it appears in logs, so an indexer built on events alone never sees it.

This tutorial queries traces with HyperSync and decodes the call data with viem, so you can read what a contract was asked to do rather than only what it logged. In a hurry, skip to the agent prompt and let your assistant do it.

Prerequisites

  • Node.js 18 or higher
  • An API token with trace access

Trace data is listed as an on-demand add-on on the HyperSync pricing page, and supported networks lists the trace chains as access on request. Ask in Discord if a trace query comes back without access.

Traces have their own endpoints

Trace data is served from a separate endpoint per chain, so the standard endpoint returns nothing for a trace query.

ChainEndpoint
Ethereumhttps://eth-traces.hypersync.xyz
Basehttps://base-traces.hypersync.xyz
Gnosishttps://gnosis-traces.hypersync.xyz

Point the client at the trace endpoint for the chain you want. These three are the trace chains in supported networks, so ask in Discord if you need another.

Set up

mkdir traces && cd traces
npm init -y
npm pkg set type=module
npm install @envio-dev/hypersync-client viem

Put your token in the environment:

export ENVIO_API_TOKEN=your_token_here

Decode the calls made to a contract

This queries every trace whose target is the WETH contract, then decodes the call data against a small ABI. decodeFunctionData throws on a selector the ABI does not cover, which is how you separate the calls you care about from the rest.

import { HypersyncClient, TraceField } from "@envio-dev/hypersync-client";
import { decodeFunctionData, parseAbi } from "viem";

const client = new HypersyncClient({
url: "https://eth-traces.hypersync.xyz",
apiToken: process.env.ENVIO_API_TOKEN,
});

const WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";

const abi = parseAbi([
"function transfer(address to, uint256 amount)",
"function transferFrom(address from, address to, uint256 amount)",
"function withdraw(uint256 amount)",
"function deposit()",
]);

const fromBlock = 20000000;

const res = await client.get({
fromBlock,
toBlock: fromBlock + 20,
traces: [{ to: [WETH] }],
fieldSelection: {
trace: [
TraceField.BlockNumber,
TraceField.TransactionHash,
TraceField.From,
TraceField.To,
TraceField.Value,
TraceField.Input,
TraceField.Type,
TraceField.Error,
],
},
});

const traces = res.data.traces ?? [];
console.log(`traces to WETH: ${traces.length}`);

for (const t of traces.slice(0, 5)) {
try {
const { functionName, args } = decodeFunctionData({ abi, data: t.input });
console.log(`${t.transactionHash} ${functionName}(${args.map(String).join(", ")})`);
} catch {
console.log(`${t.transactionHash} selector ${String(t.input).slice(0, 10)} not in the ABI`);
}
}

toBlock is exclusive, so this covers 20 blocks of Ethereum mainnet. They hold 2,893 traces to that one contract, and the decoded lines read like this:

traces to WETH: 2893
0xbb4b3fc2b746877dce70862850602f1d19bd890ab4db47e6b7ee1da1fe578a0d transferFrom(0x6b75d8AF000000e20B7a7DDf000Ba900b4009A80, 0x4fc47579eCf6Aa76677ee142B6b75FaF9EeafbA8, 17619098309492736)
0xbb4b3fc2b746877dce70862850602f1d19bd890ab4db47e6b7ee1da1fe578a0d selector 0x70a08231 not in the ABI
0xbb4b3fc2b746877dce70862850602f1d19bd890ab4db47e6b7ee1da1fe578a0d transfer(0x6b75d8AF000000e20B7a7DDf000Ba900b4009A80, 268873804657197056)

0x70a08231 is balanceOf(address), a read the ABI above does not list. Add the signature to parseAbi and it decodes too.

Tracking ETH that moved

A trace that carries a value moved ETH, including the internal transfers a contract makes, which emit no log. The server filters on from, to, address, call_type, reward_type, type and sighash, so value and error are filters you apply to the results yourself.

We have a walkthrough of that job on its own, with a call_type filter that skips staticcall and delegatecall before the data leaves the server: tracking native ETH transfers.

Reading the results

  • type tells you what the trace was. On Ethereum you see call, create and suicide, and a create has no to. The query reference also lists reward, and calls this field kind when you filter on it rather than read it
  • error is set on a call that failed, with values such as Reverted, Out of gas and Bad instruction. Failed calls still appear, which is useful when you are debugging one and noise when you are counting transfers
  • The Node client gives value as a BigInt. The HTTP API returns it as a hex string, so wrap it in BigInt if you are calling the endpoint directly
  • A plain ETH transfer carries 0x as its input, so there is nothing to decode. A call that sends ETH and calldata together carries both

The full list of trace fields and filters is in the query reference.

Hand this to your coding agent

Using HyperSync, write a Node script that fetches transaction traces from
https://eth-traces.hypersync.xyz and decodes them.

- Install @envio-dev/hypersync-client and viem, set "type": "module"
- Build the client with new HypersyncClient({ url, apiToken: process.env.ENVIO_API_TOKEN })
- Query traces filtered by the contract I name, selecting block number, transaction
hash, from, to, value, input, type and error
- Decode input with viem's decodeFunctionData against an ABI built by parseAbi,
and print the selector for any call the ABI does not cover
- Skip traces that carry an error, and treat value as a BigInt

Docs: https://docs.envio.dev/docs/HyperSync/tutorial-decoding-traces

Next steps