> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

## Introduction

This tutorial indexes ERC20 token transfers with Envio HyperIndex, using USDC on Base as the example. You end up with every `Transfer` event in a database, a running total per account, and a GraphQL API over both.

There are two ways to get there. Write the three files yourself, which is below and takes a copy and paste, or generate them from the contract address with [contract import](https://docs.envio.dev/docs/HyperIndex/quickstart), which is the rest of the page.

## The short version

Three files and a handful of commands. Everything in this section was run against Base before it was written, and the handler keeps a running total per account as well as the raw transfers.

```bash
mkdir usdc-base-transfers && cd usdc-base-transfers
pnpm init
pnpm pkg set type=module
pnpm add envio
```

The handlers are ES modules, so `type` has to be set before the indexer will load them.

Indexing reads from HyperSync, which needs an [API token](/docs/HyperSync/api-tokens). Put it in a `.env` file in the project root, or nothing will sync:

```bash
ENVIO_API_TOKEN=your_token_here
```

`config.yaml`

```yaml
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: usdc-base-transfers
contracts:
  - name: USDC
    handler: src/EventHandlers.ts
    events:
      - event: "Transfer(address indexed from, address indexed to, uint256 value)"
chains:
  - id: 8453 # Base
    start_block: 28000000
    contracts:
      - name: USDC
        address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
```

`schema.graphql`

```graphql
type Transfer {
  id: ID!
  from: String!
  to: String!
  value: BigInt!
  blockNumber: Int!
  blockTimestamp: Int!
}

type Account {
  id: ID!
  sent: BigInt!
  received: BigInt!
  transferCount: Int!
}
```

`src/EventHandlers.ts`

```typescript
import { indexer } from "envio";

indexer.onEvent(
  { contract: "USDC", event: "Transfer" },
  async ({ event, context }) => {
    context.Transfer.set({
      id: `${event.chainId}_${event.block.number}_${event.logIndex}`,
      from: event.params.from,
      to: event.params.to,
      value: event.params.value,
      blockNumber: event.block.number,
      blockTimestamp: event.block.timestamp,
    });

    const sender = await context.Account.get(event.params.from);
    context.Account.set({
      id: event.params.from,
      sent: (sender?.sent ?? 0n) + event.params.value,
      received: sender?.received ?? 0n,
      transferCount: (sender?.transferCount ?? 0) + 1,
    });

    const receiver = await context.Account.get(event.params.to);
    context.Account.set({
      id: event.params.to,
      sent: receiver?.sent ?? 0n,
      received: (receiver?.received ?? 0n) + event.params.value,
      transferCount: (receiver?.transferCount ?? 0) + 1,
    });
  },
);
```

Then start it:

```bash
pnpm envio dev
```

If you have run another indexer on this machine before, run `pnpm envio stop` first so it starts from a clean database.

Indexing starts from the block in `config.yaml`, and Hasura opens at [http://localhost:8080](http://localhost:8080) with the password `testing`.

### Querying what you indexed

The largest transfers:

```graphql
query LargestTransfers {
  Transfer(limit: 3, order_by: { value: desc }) {
    from
    to
    value
    blockNumber
  }
}
```

```json
{
  "from": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
  "to": "0xadaa772e1EEc300C1bb62D8342b69E8c627172a6",
  "value": "28060166801788",
  "blockNumber": 28000952
}
```

USDC has 6 decimals, so that value is about 28.06 million USDC.

The accounts that received the most, which comes from the `Account` entity the handler keeps updated:

```graphql
query TopReceivers {
  Account(limit: 3, order_by: { received: desc }) {
    id
    received
    transferCount
  }
}
```

```json
{
  "id": "0xb2cc224c1c9feE385f8ad6a55b4d94E92359DC59",
  "received": "2521096268603014",
  "transferCount": 24430
}
```

An aggregate like that is why you index rather than call an RPC. It is one row, kept current as new transfers arrive.

## Prefer to generate it from the contract?

The rest of this page does the same job with `envio init contract-import`, which reads the ABI from a block explorer and writes the three files for you.

## Prerequisites

Before starting, ensure you have the following installed:

- **[Node.js](https://nodejs.org/en/download/current)** _(v22 or newer, which `envio` requires)_
- **[pnpm](https://pnpm.io/installation)** _(recommended but not required)_
- **[Docker Desktop](https://www.docker.com/products/docker-desktop/)** _(required to run the Envio indexer locally)_

> **Note:** Docker is specifically required to run your blockchain indexer locally. You can skip Docker installation if you plan only to use Envio Cloud.

## Step 1: Initialize Your Indexer

`envio init` on its own prints what to do next, aimed at a coding agent as much as a person. With no token set yet it opens with that (trimmed):

```text
Welcome to Envio Indexer! Let's set up an indexer that will become a reliable blockchain backend you trust, love, and own.

Leave the rest to your favorite agent:

  1. ENVIO_API_TOKEN is not set. Ask the user to create one at https://envio.dev/app/api-tokens and provide it to the session before continuing.
  2. Prompt the user for the project intent if it is missing from context (what should the indexer track and surface?).
  3. Determine the chain, contract, and addresses needed to produce that result. Use web search or block-explorer tool calls when the user hasn't supplied them.
  4. To continue, call:

pnpx envio init contract-import explorer \
  -n ${indexer-name} \
  -c ${address} \
  -b ${chainId} \
  --single-contract \
  --all-events \
  -d ${directory}
```

Create the token first at [envio.dev/app/api-tokens](https://envio.dev/app/api-tokens) and export it, because contract import refuses to run without one:

```bash
export ENVIO_API_TOKEN=your_token_here
```

## Step 2: Import the USDC Token Contract

Fill those in for USDC on Base and run it:

```bash
pnpx envio init contract-import explorer \
  --name usdc-base-transfer-indexer \
  --contract-address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  --blockchain base \
  --single-contract \
  --all-events \
  --directory usdc-base-transfer-indexer
```

The ABI comes from the block explorer, so you only need the address. [View the contract on BaseScan](https://basescan.org/address/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913).

Drop `--all-events` to pick events yourself, and `--single-contract` to add more contracts, addresses or chains in the same pass. It finishes with:

```text
Your indexer is ready! Pick how you'd like to run it:
  1. cd usdc-base-transfer-indexer && pnpm test    # run the tests (recommended for AI)
  2. cd usdc-base-transfer-indexer && pnpm dev     # run locally
  3. cd usdc-base-transfer-indexer && pnpm start   # run in production
```

## Step 3: Start Your Indexer

1. Move into the project, and stop any indexer already running there:

```bash
cd usdc-base-transfer-indexer
pnpm envio stop
```

> **Note:** You can skip this step if this is your first time running an indexer.

2. Start your new indexer:

```bash
pnpm dev
```

This command:

- Starts the required Docker containers
- Sets up your database
- Launches the indexing process
- Opens the Hasura GraphQL interface

## Step 4: Understanding the Generated Code

Contract import writes a whole project, including tests, a `.env` and a git repo. These are the three files you will actually edit, shown here for USDC and trimmed to the `Transfer` event.

`config.yaml` holds the chain, the start block, the contract address and the events to index:

```yaml
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: usdc-base-transfer-indexer
disable_default_cross_chain: true
chains:
  - id: 8453
    start_block: 0
    contracts:
      - name: FiatTokenProxy
        address:
          - "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        events:
          - event: Transfer(address indexed from, address indexed to, uint256 value)
```

`schema.graphql` holds an entity per event, named after the contract:

```graphql
type FiatTokenProxy_Transfer {
  id: ID!
  from: String!
  to: String!
  value: BigInt!
}
```

`src/handlers/FiatTokenProxy.ts` holds a handler per event, writing one row per log:

```typescript
import { indexer } from "envio";
import type { FiatTokenProxy_Transfer } from "envio";

indexer.onEvent({ contract: "FiatTokenProxy", event: "Transfer" }, async ({ event, context }) => {
  const entity: FiatTokenProxy_Transfer = {
    id: `${event.chainId}_${event.block.number}_${event.logIndex}`,
    from: event.params.from,
    to: event.params.to,
    value: event.params.value,
  };

  context.FiatTokenProxy_Transfer.set(entity);
});
```

The entity name comes from the contract name in `config.yaml`, so renaming the contract renames the entity and the GraphQL field with it.

## Step 5: Exploring Your Indexed Data

Now you can interact with your indexed USDC transfer data:

### Accessing Hasura

1. Open Hasura at [http://localhost:8080](http://localhost:8080)
2. When prompted, enter the admin password: `testing`

### Monitoring Indexing Progress

Progress lives in two places. The **Data** tab tracks `chain_metadata`, a view with one row per chain, where `latest_processed_block` is how far indexing has reached and `block_height` is the head:

```text
 chain_id | start_block | block_height | latest_processed_block | num_events_processed
----------+-------------+--------------+------------------------+----------------------
     8453 |    28000000 |     51661447 |               28272195 |              6160136
```

The numbers above come from the hand written indexer earlier on this page. The `envio_chains` table holds the same thing with different column names, but Hasura does not track it, so read that one from Postgres directly.

> **Note:** Thanks to Envio's [HyperSync](https://docs.envio.dev/docs/hypersync), you index from the chain's own data layer rather than paging an RPC node.

### Querying Indexed Events

1. Click the **API** tab
2. Construct a GraphQL query to explore your data

Here's an example query to fetch the 10 largest USDC transfers:

```graphql
query LargestTransfers {
  FiatTokenProxy_Transfer(limit: 10, order_by: { value: desc }) {
    from
    to
    value
  }
}
```

3. Click the **Play** button to execute your query

## Hand this to your coding agent

```text
Set up an Envio HyperIndex indexer for ERC20 Transfer events.

- pnpm init, pnpm pkg set type=module, pnpm add envio, files config.yaml,
  schema.graphql, src/EventHandlers.ts
- Put ENVIO_API_TOKEN in a .env file in the project root, indexing fails without it
- config.yaml: top-level contracts block with the Transfer event signature, then a
  chains block with the chain id, start_block and the token address
- schema.graphql: a Transfer entity, plus an Account entity holding sent, received
  and transferCount
- src/EventHandlers.ts: import { indexer } from "envio" and register with
  indexer.onEvent({ contract, event }, handler). Read the account with
  context.Account.get before writing it back with context.Account.set
- Run it with pnpx envio dev, which needs Docker

Docs: https://docs.envio.dev/docs/HyperIndex/tutorial-erc20-token-transfers
```

## Common follow-ups

### How do I index more than one token?

Add another address under the same contract in `config.yaml`, or a second contract with its own handler. The [configuration file guide](/docs/HyperIndex/configuration-file) covers both.

### How do I index the same token on several chains?

Add another entry to `chains` with that chain's id, start block and token address. One indexer covers them all, see [multichain indexing](/docs/HyperIndex/multichain-indexing).

### How do I index every ERC20 rather than a list of them?

Index the `Transfer` event across all contracts with [wildcard indexing](/docs/HyperIndex/wildcard-indexing), or register tokens as they are deployed with [dynamic contracts](/docs/HyperIndex/dynamic-contracts).

### How do I get balances rather than transfers?

Keep the running total in the handler, as the `Account` entity above does. Reading a balance then becomes one row rather than a call per holder.

## Conclusion

You now have an indexer for USDC transfers on Base, either written by hand or generated from the contract, with a GraphQL API over the results.

### What You've Learned

- How to initialize an indexer using Envio's contract import feature
- How to index ERC20 token transfers on the Base chain
- How to query and analyze token transfer data using GraphQL

### Next Steps

- Try customizing the event handlers to add additional logic
- Create aggregated statistics about token transfers
- Add more tokens or events to your indexer
- Deploy your indexer to Envio Cloud

For more tutorials and advanced features, see the [documentation](https://docs.envio.dev).
