debug_traceCall: Trace a Transaction You Never Sent

debug_traceCallTrace methodEthereum JSON-RPC

debug_traceCall is eth_call with the tracer switched on. You hand it a call object and a block, the node executes it against that block's state and returns the same trace structures debug_traceTransaction produces, except nothing was ever signed or broadcast. It is the pre-flight check for anything expensive: simulate, inspect every internal call, then decide whether to send.

1 credit
per call on BLAZED.sh
Trace
call type
3
parameters
Geth, Nethermind, Erigon, Reth
client support

Try debug_traceCall

Request as curl
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "debug_traceCall",
  "params": [
    {
      "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "data": "0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045"
    },
    "latest",
    {
      "tracer": "callTracer"
    }
  ]
}'

Public endpoints keep the debug namespace switched off, so this request comes back as "the method debug_traceCall does not exist/is not available" on a shared URL. Point the curl at a node with debug APIs enabled, such as your own or the one a BLAZED.sh container sits on, and it runs as-is.

What debug_traceCall does

The mechanics are eth_call's. State changes are discarded, no gas is paid, and the call runs as whatever from address you give it. What changes is the output: instead of the return data, you get whatever the tracer you named produces, so a callTracer run returns the nested tree of internal calls with per-frame gas and revert reasons.

The two override objects are what make the method powerful. stateOverrides patches balances, nonces, code and individual storage slots for the duration of the call, so you can simulate a swap as if an approval already existed or as if your address held tokens it does not. blockOverrides patches the environment itself: block number, timestamp, base fee, coinbase. Together they let you trace a transaction against a world that does not exist yet, which is exactly what a searcher needs before committing to a bundle.

Compared with eth_estimateGas or a plain eth_call, this is the method that tells you why. A call that returns 0x, a swap that reverts with an unrecognised selector, a router that silently takes a different path than you expected: all of them become obvious the moment you can see the call tree instead of the final return value.

Parameters

#NameTypeDescription
1callObjectobjectTransaction-shaped call: to, from, value, gas, fee fields and data, exactly as eth_call takes it.
2blockParameterstringHex block number, block hash, or one of latest, pending, safe, finalized, earliest. The call is executed against the state at the end of that block.
3optionsoptionalobjecttracer (callTracer, prestateTracer, 4byteTracer or a custom tracer), tracerConfig ({ onlyTopCall, withLog, diffMode }), timeout, plus stateOverrides and blockOverrides for simulating conditions that do not currently hold.

What it returns

Whatever the chosen tracer emits, in the same shape debug_traceTransaction returns. callTracer gives one nested frame per call with type, from, to, value, gas, gasUsed, input, output and a calls array, plus error and revertReason on failing frames. prestateTracer gives the state the call read, or with diffMode the state it would have changed.

With no tracer named you get the opcode-level struct logger, which for anything but a trivial call is an enormous response. Naming a tracer is effectively mandatory.

Example response

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "type": "CALL",
    "from": "0x0000000000000000000000000000000000000000",
    "to": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
    "value": "0x0",
    "gas": "0x2faf080",
    "gasUsed": "0x2b8c",
    "input": "0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045",
    "output": "0x00000000000000000000000000000000000000000000000000000000023781bc",
    "calls": [
      {
        "type": "DELEGATECALL",
        "from": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        "to": "0x43506849d7c04f9138d1a2050bbf3a0c054402dd",
        "gasUsed": "0x1f9a",
        "input": "0x70a08231000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045",
        "output": "0x00000000000000000000000000000000000000000000000000000000023781bc"
      }
    ]
  }
}

A balanceOf call traced with callTracer, gas figures illustrative. The child frame is the giveaway that USDC is a proxy: the token address delegates to its implementation contract, something a plain eth_call would never have shown you.

debug_traceCall with ethers.js

import { WebSocketProvider } from "ethers";

// inside a BLAZED.sh container the node is one local socket away
const provider = new WebSocketProvider("ws://eth:8545");

const trace = await provider.send("debug_traceCall", [
  {
    from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    data: "0xa9059cbb" + "…", // transfer(address,uint256) calldata
  },
  "latest",
  {
    tracer: "callTracer",
    tracerConfig: { withLog: true },
    // pretend the sender is funded, without funding it
    stateOverrides: {
      "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045": { balance: "0xde0b6b3a7640000" },
    },
  },
]);

console.log(trace.error ?? "ok", trace.calls?.length ?? 0, "internal calls");

Gotchas and common errors

You are simulating against a finished block

The trace runs on the state at the end of the block you named, which means it does not include anything currently sitting in the mempool. A simulation against latest tells you what would have happened if your transaction had been the first in the next block, and the next block will not look like that. For strategies that care, either simulate against pending, or use blockOverrides to move the environment forward and accept that competing flow is invisible either way.

Overrides are the whole point, so use them

stateOverrides can hand your address a balance, replace a contract's code with an instrumented version, or set a single storage slot; blockOverrides can move the block number, timestamp or base fee. That is how you test a liquidation that is not yet profitable, a swap that needs an approval you have not made, or a time-locked path without waiting. None of it costs more than a plain trace.

The debug namespace is not on shared endpoints

Public RPCs disable debug entirely and commercial providers sell tracing as a premium add-on with separate limits. If your simulation stack depends on this method, the endpoint question is settled before anything else: you need a node where the API is simply enabled, which on BLAZED.sh is the node your container is running on.

Name a tracer or drown

Without a tracer option the response is the opcode-level struct log: every step with stack and memory, hundreds of megabytes for a complex DeFi call. callTracer answers almost every practical question in a few kilobytes, and tracerConfig.onlyTopCall shrinks it further when you do not need child frames.

It costs more work than eth_call

Tracing hooks run on every EVM step, so the same call traced is meaningfully slower than the same call executed plainly. Simulation loops that trace every candidate will feel it. A common pattern is to filter with cheap eth_call or eth_estimateGas first and trace only the candidates that survive.

Historical simulations need historical state

Tracing a call against an old block requires that block's state. Beyond the pruning window a full node cannot answer and you get "missing trie node" or an equivalent. Recent blocks are fine on tip-of-chain nodes; reconstructing what a call would have returned last quarter is an archive workload, which on BLAZED.sh is an Enterprise feature.

What debug_traceCall costs on BLAZED.sh

debug_traceCall costs 1 credit per call on BLAZED.sh, the same as a plain eth_call, and the same as debug_traceTransaction; only the Parity-style replay methods carry the 4-credit rate. The variable that moves your bill is response size, since anything over 100KB adds 50 credits per MB, and an untraced-by-default struct log is the one shape that reliably crosses it. Keep a named tracer on every request and the surcharge stays theoretical. What co-location buys here is the round-trip: pre-flight simulation only helps if the answer arrives while the opportunity is still there, which is what a sub-10ms local call gives you.

See the full credit price list

debug_traceCall: frequently asked questions

What is the difference between debug_traceCall and eth_call?

They execute the same simulation. eth_call returns the function's return data; debug_traceCall returns the execution trace, so you see every internal call, its gas and where a revert originated. Use eth_call for the answer and debug_traceCall for the reason.

Can I simulate a transaction before I have the tokens or approvals?

Yes, that is what stateOverrides is for: set a balance, a storage slot or replacement code for the duration of the call. blockOverrides does the same for the block environment, including number, timestamp and base fee.

Why does my provider reject debug_traceCall?

The debug namespace is disabled on shared endpoints, and commercial providers gate tracing behind premium plans. You need a node with the debug API enabled, such as your own or the one your code is co-located with.

Does debug_traceCall see pending transactions?

Only if you trace against the pending block, and even then it is this node's mempool view, not the block that will actually be built. Simulations against latest assume nothing else executes first.

Which tracer should I use for simulations?

callTracer for the call tree and revert hunting, with withLog: true when you want the events the call would emit; prestateTracer with diffMode when you need the state changes it would make. The opcode-level default is a last resort.

Call debug_traceCall from the node itself

Deploy your container or script next to a synced Ethereum node. No rate limits, no compute units, sub-10ms local RPC.