Overview
Exchanges, custodians, treasuries, and payment apps all need to answer “how much ETH does this address hold right now?” for a set of addresses they control or watch. Pollingeth_getBalance for every address on every block is wasteful. Most addresses do not change in most blocks, and the cost grows with the size of your address set.
The native transfers dataset tells you which addresses moved native value and in which block. A SQL function, _gs_evm_get_balances, then reads the exact balance of just those addresses at that block from Edge RPC (or from an endpoint you choose). Writing the result to Postgres leaves you with a table of current balances.
This guide shows how to build that with a Turbo pipeline:
Watch a set of addresses
Store the addresses in a dynamic table and add or remove them at any time, no redeploy.
Update on change
Get balances via SQL functions only for addresses that actually changed.
Query current balances
Land one row per address in Postgres, replaced on every change.
How it works
- The source is
ethereum.native_transfers, one row per successful native value transfer including internal transactions. It is built from raw traces, so it exists for the chains where Goldsky indexes traces. See native transfers for what it contains. - The dynamic table holds the addresses you are watching. It is a Postgres table you
INSERTinto andDELETEfrom; the pipeline picks up changes within a second or two. - A SQL transform keeps transfers where the sender or the recipient is watched, asks the RPC for both parties’ balances at the transfer’s block, and keeps the watched side.
- The Postgres sink upserts on
address, so the table holds the latest known balance for each address.
Prerequisites
- The Turbo CLI extension installed, and a Goldsky account logged in to your project.
- A Postgres database for the dynamic table and the sink. You have two options:
- Goldsky-hosted Postgres (recommended): Goldsky provisions and manages it for you. Available on Scale plans and above.
- Bring your own Postgres: Neon, Supabase, RDS, Cloud SQL, or self-hosted.
- An EVM JSON-RPC endpoint the pipeline can call. Edge RPC (recommended) lets you create an endpoint in the dashboard or via the CLI with
goldsky edge create; use its URL in the formhttps://edge.goldsky.com/standard/evm/1?key=YOUR_EDGE_KEY. Any otherhttps://endpoint works too.
The pipeline makes the RPC calls with your endpoint, so your RPC provider bills the usage. With Edge that is per request; see Edge pricing. The pipeline below makes at most two
eth_getBalance calls per watched transfer, and none for transfers that do not touch a watched address.Step 1: Provision Postgres
The dynamic table and the sink both connect to Postgres through a Goldsky secret. You can use one secret for everything in the pipeline.- Goldsky-hosted Postgres (recommended)
- Bring your own Postgres
Provision a managed database and register its secret in one step:The command prints the secret’s name, ID, and type. Look up the raw connection string in the web app under Sinks when you need to connect a SQL client. You can also provision from the web app under Sinks → New sink → Hosted Postgres.
Hosted Postgres is a Scale plan feature (and above). Adding a credit card to your account upgrades you to Scale. See pricing.
MY_POSTGRES.
Step 2: Create the pipeline
Create a file namednative-balances.yaml. Replace YOUR_EDGE_KEY with your Edge key, or replace the whole URL with another provider’s endpoint.
native-balances.yaml
What each piece does
What each piece does
sources.native_transfers: every successful native value transfer on Ethereum, including internal transactions, with lowercasedsenderandrecipientcolumns and the transferredamountin wei.start_at: latestprocesses only new transfers going forward.transforms.watched_addresses: the dynamic table.schema: publiccreatespublic.watched_addresseswith avalue(primary key) column and anupdated_attimestamp.transforms.native_balances, inner query: keeps only transfers where either party is watched, then calls_gs_evm_get_balancesonce per transfer with both addresses. The function returns a list with one{address, balance}element per input address.UNNESTturns that list into one row per address.transforms.native_balances, outer query: keeps the watched side (if both sides are watched, both rows survive), drops rows whose lookup failed, and formats the columns.balanceis a 256-bit integer;u256_to_stringrenders it as a decimal string in wei.primary_key: addresson the transform and the sink: each new transfer for an address replaces its previous row, so the table holds one current balance per address.
Step 3: Deploy
Validate, then apply:Step 4: Add addresses to watch
Connect any SQL client to the database behindMY_POSTGRES and insert the addresses you want to track. Changes take effect within a second or two.
native_balances after its first native transfer following the insert. If you need a starting balance right away, read it once from your RPC when you insert the address.
Step 5: Query current balances
Optional: keep a balance history
The table above holds one row per address. If you also want to chart how a balance changed over time, add a second Postgres sink that reads the same transform but has noprimary_key. Without a primary key the sink inserts every row instead of upserting, so you get one row per watched address per transfer.
goldsky turbo apply native-balances.yaml, then chart one address:
Coverage and limitations
Which chains have a native transfers dataset
Which chains have a native transfers dataset
native_transfers is built from raw traces, so it is available for the EVM chains with a check in the Traces column of the supported networks table. Swap ethereum.native_transfers for base.native_transfers, arbitrum_one.native_transfers, and so on, and change the URL passed to _gs_evm_get_balances to the matching chain (https://edge.goldsky.com/standard/evm/8453?key=... for Base).Balance changes that are not transfers
Balance changes that are not transfers
The pipeline reads an address’s balance again only when a native value transfer touches it. A balance can also change without one: gas paid on a transaction that sent no value, validator withdrawals, block rewards. Because every read fetches the true balance from the RPC rather than adding up transfers, the stored value catches up at the address’s next transfer, but between transfers it can drift by those amounts. If that matters for an address, refresh it on a schedule from your side.
Failed RPC lookups
Failed RPC lookups
_gs_evm_get_balances never fails the pipeline. If the endpoint is unreachable, rate limits the call, or returns an error, that address’s balance is NULL for that row and a warning is written to the pipeline logs. The AND sub.b.balance IS NOT NULL filter drops those rows so they never replace a good balance in the sink; the address is read again at its next transfer. If you remove the filter, a failed lookup overwrites the stored balance with NULL. If every balance is NULL, check the URL and key first.Token balances
Token balances
For ERC-20, ERC-721, and ERC-1155 balances use the
<chain>.balances dataset instead, which Goldsky maintains for you with no RPC calls. See EVM sources.Filtering at the source
Filtering at the source
A
filter: on the source is applied to the underlying raw traces, whose columns are from_address and to_address rather than sender and recipient. Filter in the SQL transform as shown above unless you need the source-level filter for a block range.