Skip to main content
This guide explains how to decode contract call traces on-the-fly inside a Turbo pipeline. We filter the raw_traces dataset down to a specific function using its 4-byte selector, then decode the call’s inputs and outputs with a TypeScript transform.
Turbo’s SQL functions include _gs_log_decode for decoding event logs, but there is no built-in SQL function for decoding trace calldata. The pattern in this guide (selector filtering in SQL plus a small TypeScript decoder) is the recommended way to decode traces in Turbo. To decode events instead, see Decode contract events.

What you’ll need

  1. A Goldsky account and the CLI installed
  1. Install the Goldsky CLI: For macOS/Linux:
    For Windows:
    Windows users need to have Node.js and npm installed first. Download from nodejs.org if not already installed.
  2. Log into your Project by running:
    This opens your browser to sign in (Google, GitHub, SSO, or email). Once you authenticate, the CLI is logged in automatically — there’s no API key to copy or paste.
    On a headless or remote machine (or in CI), create an API key on your Project Settings page and pass it directly with goldsky login --token <API_KEY>. Use goldsky login --no-browser to print the login URL instead of opening a browser.
  3. Now that you are logged in, run goldsky to get started:
  1. A basic understanding of Turbo pipelines
  2. A destination sink to write your data to. In this example, we will use the PostgreSQL sink

Preface

This guide shows how to decode traces of a contract with Turbo. The example uses the Friendtech contract deployed on Base (specifically calls to its getBuyPriceAfterFee(address,uint256) function), but the same logic applies to any other contract and chain for which a raw_traces dataset is available (see supported chains). Two columns of the raw traces schema (EVM schemas) hold the encoded call data:
  • input: the data sent along with the message call: 0x, followed by the 4-byte function selector (8 hex characters), followed by the ABI-encoded arguments as 32-byte words (64 hex characters each).
  • output: the data returned by the message call: 0x followed by the ABI-encoded return values as 32-byte words.
Knowing this layout, decoding a call with statically-sized arguments (addresses, integers, booleans) is just slicing hex strings.

Pipeline definition

traces-decoding-pipeline.yaml
There are two transforms in this pipeline definition which are responsible for decoding the contract calls; we’ll explain how they work in detail. If you copy and use this configuration file, make sure to update:
  1. Your secret_name. If you already created a secret, you can find it via the CLI command goldsky secret list.
  2. The schema and table you want the data written to. By default it writes to decoded_traces.friendtech.

Filtering by function selector

Let’s start analyzing the first transform:
Transform: buy_price_calls
The WHERE clause does two things:
  1. to_address = ... keeps only traces where the Friendtech contract is the call target.
  2. The substr comparison keeps only calls to the function we care about. _gs_keccak256 computes the keccak256 hash of the canonical function signature, and the first 10 characters (0x plus 8 hex characters) are the 4-byte selector that prefixes every call’s input.
If you already know the selector, you can paste the literal hex value instead of computing it, but deriving it from the signature keeps the query self-documenting.

Decoding transform

The second transform is a TypeScript transform that slices the ABI-encoded hex into named, typed columns:
  • Inputs: after skipping 0x and the 8-character selector, each argument occupies one 32-byte word. The address argument (sharesSubject) is the last 40 hex characters of its word; the uint256 argument (amount) is the whole word, converted to a decimal string with JavaScript’s native BigInt so large values don’t lose precision.
  • Outputs: return data is encoded the same way but has no selector, so the function’s single uint256 return value is the first 64 hex characters after 0x.
  • Filtering: returning null from invoke drops the record, which we use to discard calls with no return data (for example, reverted calls).
The schema field declares the transform’s output columns, which is what the Postgres sink will create as the table structure.
This slicing approach works for statically-sized argument types: address, uintN / intN, bool, and bytesN. Dynamically-sized types (string, bytes, arrays) are encoded as offset pointers into the calldata and need extra logic to follow. For complex signatures, consider decoding in your own service via an HTTP handler transform instead.

Deploying the pipeline

As a last step, to deploy this pipeline and start sinking decoded data into your database simply execute:
You can verify the decoded output in real time with live inspect:

Conclusion

In this guide we have explored an example implementation of how to decode raw traces and stream them into a PostgreSQL database using a selector filter in SQL and a TypeScript decoding transform. This same methodology can be applied to any contract of interest on any chain with raw_logs and raw_traces datasets available (see supported chains). Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.