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

![How to Index USDC Transfers on Arc](/blog-assets/index-arc-usdc-transfers.png)

<!--truncate-->

:::note TL;DR

- USDC is Arc's gas token. Every USDC transfer is logged as a standard `Transfer` from the system address `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`, in **18 decimals**. The USDC contract at `0x3600…0000` logs a second copy of ERC-20 calls, in 6 decimals.
- Index the system address and nothing else. In a 100,000 block sample, indexing both would have counted **48.3%** of transfers twice, and indexing only the contract would have missed **51.7%**.
- Arc is chain ID `5042`, on [HyperSync](https://docs.envio.dev/docs/HyperSync/overview) at `https://arc.hypersync.xyz`. Endpoints and testnet details are on our [Arc chain page](https://envio.dev/chains/arc).
- The indexer below is three files and matched HyperSync's count exactly. Copy it, clone it from [GitHub](https://github.com/enviodev/arc-usdc-indexer), or [hand the build to your coding agent](#build-it-with-your-coding-agent) with our prompt.

:::

In a 14 hour sample of Arc blocks, 906,915 USDC transfers were logged, and 469,254 of them never touched the USDC contract. They were native movements, like a plain send from one wallet to another, and on Arc those get a standard `Transfer` log from a system address.

[Arc](https://www.arc.io) is an open Layer 1 blockchain that uses USDC as its gas token, so USDC is the native coin and an ERC-20 at the same time. It implements [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708), which logs native movements the same way as token transfers. On a standard EVM chain, a plain native send leaves no log at all. The one thing an indexer has to get right is which stream to count. This guide covers that, then builds a small indexer using Envio's [HyperIndex](https://docs.envio.dev/docs/HyperIndex/overview), the base for payment tracking, treasury reconciliation or any USDC dashboard on Arc.

## Connecting to Arc

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <colgroup><col style={{width: "34%"}}/><col style={{width: "66%"}}/></colgroup>
  <thead>
    <tr><th>Property</th><th>Value</th></tr>
  </thead>
  <tbody>
    <tr><td>Chain ID</td><td><code>5042</code> (mainnet), <code>5042002</code> (testnet)</td></tr>
    <tr><td>Gas token</td><td>USDC</td></tr>
    <tr><td>HyperSync (mainnet)</td><td><code>https://arc.hypersync.xyz</code></td></tr>
    <tr><td>HyperSync (testnet)</td><td><code>https://arc-testnet.hypersync.xyz</code></td></tr>
    <tr><td>HyperRPC (mainnet, read only)</td><td><code>https://arc.rpc.hypersync.xyz</code></td></tr>
    <tr><td>HyperRPC (testnet, read only)</td><td><code>https://arc-testnet.rpc.hypersync.xyz</code></td></tr>
    <tr><td>Public RPC (mainnet)</td><td><code>https://rpc.mainnet.arc.io</code></td></tr>
    <tr><td>Public RPC (testnet)</td><td><code>https://rpc.testnet.arc.io</code></td></tr>
    <tr><td>Explorer</td><td><a href="https://explorer.arc.io">explorer.arc.io</a> (mainnet), <a href="https://explorer.testnet.arc.io">explorer.testnet.arc.io</a> (testnet)</td></tr>
  </tbody>
</table>

Arc and Arc Testnet both have first-class support on Envio, so HyperSync serves their data and HyperIndex doesn't need an RPC to index them. The public RPCs and explorers come from Arc's [connection docs](https://docs.arc.io/arc/references/connect-to-arc), and more details are on our [Arc](https://envio.dev/chains/arc) and [Arc Testnet](https://envio.dev/chains/arc-testnet) chain pages.

Blocks come about every half second. Order by block number rather than timestamp, because several blocks can share one. Finality is deterministic, so there are no reorgs to handle.

## How Arc Logs USDC Transfers

USDC has one balance on Arc and two ways to move it. Arc's [USDC system events reference](https://docs.arc.io/arc/references/usdc-system-events) covers both.

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr><th>Stream</th><th>Emitter</th><th>Decimals</th><th>What it covers</th></tr>
  </thead>
  <tbody>
    <tr><td>Native USDC</td><td><code>0xffff…fFfE</code></td><td>18</td><td>Every USDC transfer, including native sends, mints and burns</td></tr>
    <tr><td>ERC-20 USDC</td><td><code>0x3600…0000</code></td><td>6</td><td>Only calls made through the ERC-20 interface</td></tr>
  </tbody>
</table>

The system address isn't a contract, just the address Arc's EIP-7708 logs come from. An ERC-20 `transfer()` on USDC produces a log from both addresses, while a plain native send produces only the system one.

Here's what we counted over blocks 22,477,236 to 22,577,235.

<table style={{display: "table", width: "100%", tableLayout: "fixed", overflow: "visible", wordBreak: "break-word"}}>
  <thead>
    <tr><th>What we counted</th><th>Logs</th><th>Share of system logs</th></tr>
  </thead>
  <tbody>
    <tr><td><code>Transfer</code> from the system address</td><td>906,915</td><td>100%</td></tr>
    <tr><td>Also logged by the USDC contract</td><td>437,661</td><td>48.3%</td></tr>
    <tr><td>Only logged by the system address</td><td>469,254</td><td>51.7%</td></tr>
  </tbody>
</table>

*Matched on transaction hash, sender, receiver and value, with the 6 decimal value scaled up by 10¹². Read through HyperSync.*

The USDC contract logged 446,270 transfers in the same window. All but 8,609 matched a system log, and every one of those 8,609 was either a zero value transfer (4,808) or a transfer to yourself (3,801). No USDC moved in any of them, and Arc's docs say the system address skips both.

**21.7%** of the system transfers, 197,025 of them, carried digits past the sixth decimal place, which a 6 decimal view can't represent. The window also had 1,013 mints and 446 burns, logged as transfers from and to the zero address.

For a USDC total, index the system address and nothing else. If you need to know which transfers went through the ERC-20 interface, join the contract's logs the same way we matched them above, but don't add them to the count.

## Build It With Your Coding Agent

The quickest way to build this is to hand it to a coding agent. We wrote the prompt below from the finished indexer, so it already knows the traps. Give the agent our current docs first so it works from live syntax, then paste the prompt. If you'd rather build it by hand, the [steps are below](#building-the-arc-usdc-indexer).

```bash
claude mcp add --transport http envio-docs https://docs.envio.dev/mcp
```

Cursor and VS Code use the same endpoint, and an agent with shell access can use `envio tools search-docs` instead. Both are on the [MCP server page](https://docs.envio.dev/docs/HyperIndex/mcp-server).

<details>
<summary>Show the full prompt</summary>

```text
Build me an Envio HyperIndex indexer that counts every USDC transfer on Arc
exactly once.

Before writing code, read the current HyperIndex docs rather than working from
memory. Use the envio-docs MCP server (docs_search, docs_fetch) if you have it,
otherwise `envio tools search-docs <query>`. Look up the configuration file,
schema and event handlers.

THE CHAIN

Arc is chain ID 5042. HyperSync is the default data source, so the config needs
no rpc block. USDC is Arc's native gas token.

WHICH ADDRESS TO INDEX

Every USDC transfer on Arc, including native sends, mints and burns, is logged
as a standard
  Transfer(address indexed from, address indexed to, uint256 value)
from the system address 0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE, with values
in 18 decimals. Put that address in config.yaml as if it were a contract.

Do not also index Transfer from the USDC ERC-20 contract at
0x3600000000000000000000000000000000000000. It logs a second copy of every
ERC-20 interface transfer, in 6 decimals, so adding it double counts. Never mix
the 6 and 18 decimal values.

Mints are transfers from the zero address and burns are transfers to it. The
system address never logs zero value transfers or transfers to yourself.

WHAT TO STORE

Per account, net USDC movement across the indexed window (call it net movement,
not balance, because anything held before the start block is invisible), plus
transfers in and transfers out. The zero address is not an account, so it gets
no row. Per UTC day, transfer count, volume, and the number of mints and burns.

SETUP

  mkdir arc-usdc-indexer && cd arc-usdc-indexer
  pnpm init
  pnpm add envio --allow-build=esbuild
  pnpm add -D typescript @types/node
  npm pkg set type=module

Handlers auto-load from src/handlers/. Put an Envio API token from
https://envio.dev/app/api-tokens in .env as ENVIO_API_TOKEN and keep .env out of
git. For a quick first run, set start_block about 100,000 blocks below the
current height from https://arc.hypersync.xyz/height. Run `pnpm envio codegen`
then `pnpm envio dev`, with Docker running.

TRAPS

- Use Node.js 22 or newer. Older versions fail to auto-load handlers.
- Neither codegen nor dev typechecks. Add a tsconfig.json that includes src and
  the generated envio-env.d.ts, and run `pnpm exec tsc --noEmit`.
- If `envio dev` says the config is incompatible with existing indexer data,
  the local Postgres holds another indexer. Don't reset it. Run with
  ENVIO_PG_SCHEMA and ENVIO_INDEXER_PORT set to new values instead.
- If you sort or bucket events, use block number, not timestamp. Arc blocks are
  about half a second apart and can share a timestamp.
- On Arc Testnet (5042002), native USDC history from before the Zero5 hard fork
  uses NativeCoin* events from 0x1800000000000000000000000000000000000000
  instead. Mainnet has used the system Transfer log since genesis.

REPORT BACK

Progress is in the envio_chains table (progress_block, source_block,
events_processed). Report the exact block range you indexed, the number of
Transfer events processed, the number of accounts, and mints and burns. Give
the numbers you actually got. Don't assume ours still hold.
```

</details>

## Building the Arc USDC Indexer

:::tip New to Envio?

Check the [prerequisites](https://docs.envio.dev/docs/HyperIndex/quickstart#prerequisites) before running anything below.

:::

The [Arc USDC indexer](https://github.com/enviodev/arc-usdc-indexer) is three files. `config.yaml` names the chain and the event, `schema.graphql` defines what gets stored, and one handler file does the work. [HyperIndex](https://docs.envio.dev/docs/HyperIndex/overview) reads the logs from HyperSync and gives you a GraphQL API over the result. You'll also need a free [Envio API token](https://envio.dev/app/api-tokens) in a `.env` file as `ENVIO_API_TOKEN`.

```bash
mkdir arc-usdc-indexer && cd arc-usdc-indexer
pnpm init
pnpm add envio --allow-build=esbuild
pnpm add -D typescript @types/node
npm pkg set type=module
```

### config.yaml

The system address goes in as if it were a contract. Arc is on HyperSync, so there's no `rpc` block to configure. Start from a recent block so a first run is quick. `curl https://arc.hypersync.xyz/height` returns the current height, and 100,000 blocks is about 14 hours of chain time. The config below uses the first block of our sample. Set `start_block` to `0` for everything since genesis, and budget for a much longer sync.

```yaml
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: arc-usdc-indexer
description: Every USDC transfer on Arc (chain 5042)

contracts:
  # Arc's system address logs a standard Transfer for every USDC movement,
  # with values in 18 decimals. It is not a deployed contract.
  - name: NativeUSDC
    events:
      - event: "Transfer(address indexed from, address indexed to, uint256 value)"

chains:
  - id: 5042 # Arc. HyperSync is the default data source.
    start_block: 22477236 # the first block of our sample. Use a recent block.
    contracts:
      - name: NativeUSDC
        address: "0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE"
```

### schema.graphql

The [schema](https://docs.envio.dev/docs/HyperIndex/schema) keeps totals per account and per day. `netChange` is movement across the indexed window, not a balance, because anything held before the start block is invisible to the indexer.

```graphql
type Account {
  id: ID! # account address
  # Net USDC movement across the indexed window, 18 decimals. Not a balance.
  netChange: BigInt!
  transfersIn: Int!
  transfersOut: Int!
}

type DailyStat {
  id: ID! # day index
  transfers: Int!
  volume: BigInt!
  mints: Int!
  burns: Int!
}
```

### The Handler

The [handler](https://docs.envio.dev/docs/HyperIndex/event-handlers) goes in `src/handlers/NativeUSDC.ts`. Values stay in 18 decimals, so divide by 10¹⁸ when you display them.

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

const ZERO = "0x0000000000000000000000000000000000000000";
const DAY = 86_400;

indexer.onEvent(
  { contract: "NativeUSDC", event: "Transfer" },
  async ({ event, context }) => {
    const { from, to, value } = event.params;
    const day = String(Math.floor(event.block.timestamp / DAY));

    // The system emitter skips self transfers, so from and to always differ.
    // Mints come from the zero address and burns go to it.
    const [sender, receiver, stat] = await Promise.all([
      from === ZERO ? undefined : context.Account.get(from),
      to === ZERO ? undefined : context.Account.get(to),
      context.DailyStat.getOrCreate({
        id: day,
        transfers: 0,
        volume: 0n,
        mints: 0,
        burns: 0,
      }),
    ]);

    if (from !== ZERO) {
      context.Account.set({
        id: from,
        netChange: (sender?.netChange ?? 0n) - value,
        transfersIn: sender?.transfersIn ?? 0,
        transfersOut: (sender?.transfersOut ?? 0) + 1,
      });
    }
    if (to !== ZERO) {
      context.Account.set({
        id: to,
        netChange: (receiver?.netChange ?? 0n) + value,
        transfersIn: (receiver?.transfersIn ?? 0) + 1,
        transfersOut: receiver?.transfersOut ?? 0,
      });
    }

    context.DailyStat.set({
      ...stat,
      transfers: stat.transfers + 1,
      volume: stat.volume + value,
      mints: stat.mints + (from === ZERO ? 1 : 0),
      burns: stat.burns + (to === ZERO ? 1 : 0),
    });
  },
);
```

### Running It

```bash
pnpm envio codegen
pnpm envio dev
```

`envio dev` starts Postgres and Hasura in Docker and begins syncing. Over the same 100,000 blocks, our run processed **906,915 events**, exactly the number HyperSync returned, across 106,129 accounts.

Once it's running, `http://localhost:8080` opens a [Hasura console](https://docs.envio.dev/docs/HyperIndex/navigating-hasura), and the local admin secret is `testing`. This query returns the accounts with the largest net inflow over the window.

```graphql
query TopReceivers {
  Account(order_by: { netChange: desc }, limit: 10) {
    id
    netChange
    transfersIn
    transfersOut
  }
}
```

The finished indexer is on GitHub at [enviodev/arc-usdc-indexer](https://github.com/enviodev/arc-usdc-indexer), along with the script behind the numbers above. For stablecoins on other chains, see [how to index and track stablecoin transfers on Solana](https://docs.envio.dev/blog/index-solana-stablecoin-transfers) and [how to build an open source RWA stablecoin dashboard](https://docs.envio.dev/blog/how-to-build-rwa-dashboard).

When you want it online, push the project to GitHub and deploy it on [Envio Cloud](https://docs.envio.dev/docs/HyperIndex/hosted-service), which runs the database, the GraphQL API and the syncing for you.

## Frequently Asked Questions

### Which Address Should I Index for USDC Transfers on Arc?

Index `Transfer` from the system address `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`. It logs every USDC transfer, including native sends, mints and burns, in 18 decimals. The USDC contract at `0x3600…0000` only logs ERC-20 interface calls, and those also appear in the system stream. In our 100,000 block sample, indexing only the contract missed 51.7% of transfers, and indexing both double counted 48.3%.

### Why Does USDC on Arc Have Both 18 and 6 Decimals?

USDC on Arc has one balance with two interfaces. The native interface, used for gas and plain value transfers, has 18 decimals. The ERC-20 interface at `0x3600000000000000000000000000000000000000` has 6 decimals and is there for things like `approve` and `transferFrom`. Raw values differ by 10¹², and the 6 decimal view truncates any digits past the sixth decimal place, which applied to 21.7% of the transfers in our sample.

### Why Don't Gas Fees Show Up as USDC Transfers on Arc?

Gas on Arc is paid in USDC, but gas deductions don't emit a `Transfer` log, so an indexer built on transfer events won't see them. Arc's [USDC system events page](https://docs.arc.io/arc/references/usdc-system-events) says to work out gas from the transaction receipt as `gasUsed × effectiveGasPrice`. If you're reconciling an account's USDC, take gas from receipts separately from the transfers you index.

### How Do I Match a Payment Memo to a USDC Transfer on Arc?

Arc's [Memo contract](https://docs.arc.io/arc/concepts/transaction-memos) at `0x5294E9927c3306DcBaDb03fe70b92e01cCede505` lets a wallet attach a reference, like an invoice ID, to a call such as a USDC transfer. It emits `Memo(address indexed sender, address indexed target, bytes32 callDataHash, bytes32 indexed memoId, bytes memo, uint256 memoIndex)` in the same transaction. Add the contract to `config.yaml` with that event, list `hash` in the [`fields` option](https://docs.envio.dev/docs/HyperIndex/event-handlers#selecting-block-and-transaction-fields) of both the Memo and `NativeUSDC` handlers to read `event.transaction.hash`, and match memos to transfers on transaction hash. If a transaction holds more than one transfer, compare `callDataHash` with the hash of the transfer calldata, as Arc's memo docs suggest. Match on the system `Transfer` log rather than the memo alone, since a memo can also wrap a call that moves no USDC, like a zero value transfer.

### How Do I Index USDC on Arc Testnet?

Change the chain ID to `5042002` and set a testnet `start_block` from `https://arc-testnet.hypersync.xyz/height`. Arc Testnet logs USDC transfers from the same system address. Before the Zero5 hard fork, testnet logged native USDC movements as `NativeCoin*` events from `0x1800000000000000000000000000000000000000` instead, so a backfill across that point needs both. Arc's [USDC system events page](https://docs.arc.io/arc/references/usdc-system-events) has the event signatures, and testnet USDC comes from [Circle's faucet](https://faucet.circle.com).

### How Do I Index EURC or Other Tokens on Arc?

The same way as on any EVM chain. EURC at `0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1` and other ERC-20 tokens on Arc emit `Transfer` from their own contract addresses, so you add each contract to `config.yaml` as usual. Arc's [contract addresses page](https://docs.arc.io/arc/references/contract-addresses) lists the official tokens.

## Build With Envio

Envio is a real-time multichain blockchain indexer that turns onchain events into a queryable GraphQL API. Arc has first-class support, so HyperSync, HyperRPC and HyperIndex all work on it out of the box. [Start indexing Arc](https://envio.dev/chains/arc), deploy on Envio Cloud or self-host, and if you're building on Arc, come talk to us about your data needs.

[Subscribe to our newsletter](https://envio.beehiiv.com/subscribe?utm_source=envio.beehiiv.com&utm_medium=newsletter&utm_campaign=new-post)

[Website](https://envio.dev/) | [X](https://twitter.com/envio_indexer) | [Discord](https://discord.gg/envio) | [Telegram](https://t.me/+5mI61oZibEM5OGQ8) | [GitHub](https://github.com/enviodev) | [YouTube](https://www.youtube.com/channel/UCR7nZ2yzEtc5SZNM0dhrkhA) | [Reddit](https://www.reddit.com/user/Envio_indexer)
