> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bloklake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Index ERC-20 transfers

> Backfill every transfer of a token into Parquet, decoded, then query it with DuckDB.

This guide reads every `Transfer` of USDC over a block range, decodes sender, receiver and
amount, and writes them to a Parquet file. Pointed at the token's deployment block and the head,
the same code gives you the token's complete history.

It reads Arrow directly with the [Arrow reader](https://docs.bloklake.com/responses/reading-arrow.md#complete-readers)
(`bloklake_arrow.py`), because Arrow tables go into Parquet without conversion. If you'd rather
work with JSON rows, the [client libraries](https://docs.bloklake.com/clients.md) take the same query.

## The query

```python theme={null}
USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

body = {
    "logs": [{"address": [USDC], "topic0": [TRANSFER]}],
    "fields": {"log": {"blockNumber": True, "logIndex": True, "transactionHash": True,
                       "blockTimestamp": True, "topic1": True, "topic2": True, "data": True}},
}
```

* `topic1` and `topic2` are the indexed `from` and `to`. `data` is the `uint256` amount.
* `topic0` and `topic3` are left out: you already know `topic0`, and `topic3` is always null for
  ERC-20 `Transfer`. Fewer columns means [lower cost](https://docs.bloklake.com/billing/how-billing-works.md).
* `blockTimestamp` puts the time on each row, so you don't need a blocks stream.

## Backfill into Parquet

```python theme={null}
import pyarrow as pa
import pyarrow.parquet as pq
from bloklake_arrow import query

START, END = 19426000, 19427499          # use the token's deployment block and the head

def decode(t: pa.Table) -> pa.Table:
    """Transfer(address indexed from, address indexed to, uint256 value)."""
    return pa.table({
        "block_number": t["block_number"],
        "log_index": t["log_index"],
        "tx_hash": t["transaction_hash"],
        "from": pa.array([b[12:] for b in t["topic1"].to_pylist()], pa.binary(20)),
        "to": pa.array([b[12:] for b in t["topic2"].to_pylist()], pa.binary(20)),
        # uint256 doesn't fit any Arrow integer type: keep it as a decimal string
        "value": pa.array([str(int.from_bytes(b, "big")) for b in t["data"].to_pylist()]),
    })

writer, frm, total = None, START, 0
while frm <= END:
    tables, meta = query({**body, "fromBlock": frm, "toBlock": END})
    part = decode(tables["logs"])
    writer = writer or pq.ParquetWriter("usdc_transfers.parquet", part.schema, compression="zstd")
    writer.write_table(part)
    total += part.num_rows
    frm = int(meta["bl.next_block"])
writer.close()
print(f"{total} transfers up to block {END}")
```

```text theme={null}
17189 transfers up to block 19427499
```

Each loop iteration is one [page](https://docs.bloklake.com/querying/pagination.md): up to 100,000 transfers, ending on a
block boundary. Memory stays flat however long the range is.

## Query it

```python theme={null}
import duckdb

duckdb.sql("""
    select '0x' || lower(hex("to")) as receiver, count(*) as transfers,
           sum(cast(value as hugeint)) / 1e6 as usdc
    from 'usdc_transfers.parquet'
    group by 1 order by usdc desc limit 3
""").show()
```

## Going further

* **Many tokens**: put all their addresses in `address`, or in a [set](https://docs.bloklake.com/querying/sets.md) if there
  are thousands. Keep `address` in your output to tell them apart.
* **Every ERC-20 on the chain**: drop `address` and filter on `topic0` alone. ERC-721 transfers
  share the same `topic0` but have a third indexed argument (`topic3` = token id) and empty
  `data`; tell them apart with `topic3` or the `data` length.
* **Resuming**: save `next_block` after each page. On restart, continue from it. Pages are
  deterministic for finalized blocks, so you never double-count.
* **Keeping up with the head**: switch to the loop in
  [Following the head](https://docs.bloklake.com/guides/follow-the-head.md) once the backfill catches up.
* **Faster pages**: after the first page, send only the
  [prepared query id](https://docs.bloklake.com/querying/prepared-queries.md) and the range.

## Cost

You pay for the bytes of the rows you receive. Here each row is about 160 bytes in Arrow
(block number 8, log index 4, hash 32, timestamp 8, two topics 64, `data` about 40). A million
transfers is about 160 MB: **about \$0.08 on pay-as-you-go**. See
[Estimating costs](https://docs.bloklake.com/billing/estimating-costs.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.