For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content
Tutorials

How to Index USDC Transfers on Arc

Author:Jordyn LaurierJordyn Laurier··13 min read
Reviewed by:Kenau Vith

How to Index USDC Transfers on Arc

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 at https://arc.hypersync.xyz. Endpoints and testnet details are on our Arc chain page.
  • The indexer below is three files and matched HyperSync's count exactly. Copy it, clone it from GitHub, or hand the build to 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 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, 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, the base for payment tracking, treasury reconciliation or any USDC dashboard on Arc.

Connecting to Arc​

PropertyValue
Chain ID5042 (mainnet), 5042002 (testnet)
Gas tokenUSDC
HyperSync (mainnet)https://arc.hypersync.xyz
HyperSync (testnet)https://arc-testnet.hypersync.xyz
HyperRPC (mainnet, read only)https://arc.rpc.hypersync.xyz
HyperRPC (testnet, read only)https://arc-testnet.rpc.hypersync.xyz
Public RPC (mainnet)https://rpc.mainnet.arc.io
Public RPC (testnet)https://rpc.testnet.arc.io
Explorerexplorer.arc.io (mainnet), explorer.testnet.arc.io (testnet)

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, and more details are on our Arc and 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 covers both.

StreamEmitterDecimalsWhat it covers
Native USDC0xffff…fFfE18Every USDC transfer, including native sends, mints and burns
ERC-20 USDC0x3600…00006Only calls made through the ERC-20 interface

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.

What we countedLogsShare of system logs
Transfer from the system address906,915100%
Also logged by the USDC contract437,66148.3%
Only logged by the system address469,25451.7%

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.

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.

Show the full prompt
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.

Building the Arc USDC Indexer​

New to Envio?

Check the prerequisites before running anything below.

The 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 reads the logs from HyperSync and gives you a GraphQL API over the result. You'll also need a free Envio API token in a .env file as ENVIO_API_TOKEN.

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-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 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.

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 goes in src/handlers/NativeUSDC.ts. Values stay in 18 decimals, so divide by 10¹⁸ when you display them.

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​

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, and the local admin secret is testing. This query returns the accounts with the largest net inflow over the window.

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

The finished indexer is on GitHub at 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 and how to build an open source RWA stablecoin dashboard.

When you want it online, push the project to GitHub and deploy it on Envio Cloud, 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 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 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 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 has the event signatures, and testnet USDC comes from Circle's faucet.

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 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, 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

Website | X | Discord | Telegram | GitHub | YouTube | Reddit