Real-time wallet transfer monitor for the Arc Network. Tracks native and ERC20 transfers for a defined set of wallets with sub-second latency, O(1) per-block processing, and no dependency on hosted indexing services.
Built by Devancore — post-trade operations infrastructure for institutional capital markets.
Arc's deterministic finality changes what monitoring means.
On probabilistic chains, you watch for transfers and then wait — counting confirmation blocks, estimating re-org risk, deciding when "probably final" is final enough. On Arc, one committed block is irreversible. There is no confirmation depth to tune, no re-org to handle, no probabilistic state to manage. A transfer either happened or it didn't.
Arc Feed is built around that guarantee. It does not retry blocks for confirmation. It does not track "pending" state. When the processor receives a block, it fetches the data, matches it against your wallets, and emits the result — because on Arc, that is all that is needed.
Arc Feed is self-hosted, open-source, and Arc-native. It is designed for operators — firms building on Arc who need to track their own wallets for operations, reconciliation, or compliance reporting.
npm install
# Configure environment
cp .env.example .env
# Edit .env with your RPC endpoint and database credentials
# Start Postgres
docker compose up -d
# Set up the database schema
npx prisma db push
npx prisma generate
# Add wallets to track
npx prisma studio
# → Add Arc wallet addresses to the Wallet table
# Start
npm startArc Feed connects to an Arc RPC node and processes each new block as it is produced. For every block, it makes 2 batched RPC calls (issued as a single HTTP request) to fetch transactions and Transfer event logs, then matches them against your tracked wallets.
Arc RPC
│
▼
┌──────────┐
│ watcher │ Polls for new blocks (backfill → live)
└────┬─────┘
│ blockNumber
▼
┌──────────┐
│ processor│ Promise.all([getBlock, getLogs]) per block
└──┬────┬──┘
│ │
▼ ▼
┌──────┐ ┌──────┐
│native│ │erc20 │ Match against in-memory wallet Set
└──┬───┘ └──┬───┘
│ │
▼ ▼
┌─────────────────┐
│ output │ Structured log + colorized console
└─────────────────┘
Performance characteristics:
- O(1) per block regardless of wallet count — wallet matching uses an in-memory
Set<string>with O(1) lookups - 2 RPC calls per block regardless of wallet count —
getLogsfetches all Transfer events for the block, filtering happens in-memory - AsyncGenerator for block watching — natural backpressure, no unbounded buffering
- Per-block error recovery — transient RPC failures are caught and logged; processing continues with the next block
All configuration via environment variables (.env file supported):
| Variable | Required | Default | Description |
|---|---|---|---|
RPC_URL |
Yes | — | Arc RPC endpoint (HTTP/HTTPS) |
WS_URL |
No | — | Arc WebSocket endpoint. When set, Arc Feed uses WebSocket transport for lower-latency block delivery instead of polling |
DATABASE_URL |
Yes | — | PostgreSQL connection string |
DB_USER |
Yes | — | PostgreSQL user (Docker Compose) |
DB_PASSWORD |
Yes | — | PostgreSQL password (Docker Compose) |
DB_NAME |
Yes | — | PostgreSQL database name (Docker Compose) |
POLLING_INTERVAL_MS |
No | 2000 |
Block polling interval in ms (HTTP transport only) |
START_BLOCK |
No | — | Backfill from this block number |
LOG_LEVEL |
No | INFO |
DEBUG or INFO |
HEARTBEAT_INTERVAL |
No | 50 |
Blocks between heartbeat log lines |
Arc Feed supports two transport modes:
HTTP polling (default) — polls the RPC endpoint every POLLING_INTERVAL_MS. Simple, robust, works with any Arc RPC node. Sufficient for most operational use cases given Arc's block time.
WebSocket — set WS_URL to use WebSocket transport. Eliminates polling latency; the node pushes new blocks as they arrive. Recommended for latency-sensitive applications or when you need the tightest possible alignment with Arc's block production.
Tracked wallets are stored in PostgreSQL and loaded into memory at startup:
# Open Prisma Studio (GUI) to add/remove wallets
npx prisma db push
npx prisma studio
# Or manage directly via psql / any PostgreSQL client
INSERT INTO "Wallet" (address) VALUES ('0xYourArcWalletAddress');Wallets are matched case-insensitively against transaction senders, recipients, and Transfer event log participants. Restart the process after adding wallets to reload the in-memory set.
Arc Feed detects two transfer types:
Native transfers — Arc's native gas token sent via regular transactions where tx.value > 0.
ERC20 transfers — Token transfers emitted as Transfer(address indexed from, address indexed to, uint256 value) from any ERC20-compatible contract on Arc. This includes USDC (Circle's preferred settlement asset on Arc) and any other token deployed to the network.
Each detected transfer is structured as:
interface Transfer {
kind: "native" | "erc20";
blockNumber: bigint;
txHash: Hash;
from: Address;
to: Address;
wallet: Address; // which tracked wallet matched
direction: "in" | "out"; // relative to the tracked wallet
value: bigint;
timestamp: bigint;
// ERC20 only:
token?: Address; // token contract address
logIndex?: number;
}Direction logic:
- IN — tracked wallet is the recipient (
wallet === to) - OUT — tracked wallet is the sender (
wallet === from) - If both
fromandtoare tracked wallets, two transfers are emitted — one IN and one OUT
To process historical blocks before starting live monitoring, set START_BLOCK:
START_BLOCK=30264990Arc Feed processes all blocks from START_BLOCK to the current head, then transitions to live monitoring. Useful for:
- Onboarding a wallet that was active before the parser was running
- Reconstructing transfer history for reconciliation or audit
- Replaying a block range after a schema change
INFO level (default) — one line per block with a transfer, plus heartbeat:
[2026-03-05T12:00:00.000Z] INFO Starting Arc Feed | {"wallets":3,"rpc":"https://rpc.arc.network"}
[2026-03-05T12:00:02.123Z] INFO Block #30264990 | {"ms":142,"native":1,"erc20":3}
[2026-03-05T12:01:30.000Z] INFO Heartbeat | {"blocks":"30264990-30265039","processed":50,"transfers":12,"errors":0,"avgMs":112}
DEBUG level (LOG_LEVEL=DEBUG) — additionally logs every empty block:
[2026-03-05T12:00:02.456Z] DEBUG Block #30264991 | {"ms":98,"transfers":0}
The heartbeat fires every HEARTBEAT_INTERVAL blocks and includes: block range, total blocks processed, transfers found, errors, and average processing time per block.
Arc Feed emits structured transfer events. What you do with them is up to you. The current implementation writes colorized output to the console — extend it to fit your stack:
Persist transfers to PostgreSQL — add a Transfer model to prisma/schema.prisma and write records in the output handler. The Prisma client and connection are already available.
Webhook delivery — POST each transfer to an internal endpoint. Useful for triggering downstream workflows: position updates, reconciliation checks, compliance screening.
Feed to a compliance vendor — pipe transfer events to Elliptic or TRM Labs for real-time AML screening. Arc Feed gives you the raw event; the compliance vendor gives you the risk signal.
Operations dashboard — stream transfers to a time-series store (InfluxDB, TimescaleDB) and visualize wallet activity in Grafana or similar.
Reconciliation trigger — on each detected transfer, query your internal books-of-record and flag any discrepancy between on-chain state and recorded position.
The current Prisma schema has one model: Wallet. To persist transfers, add to prisma/schema.prisma:
model Transfer {
id String @id @default(cuid())
kind String // "native" | "erc20"
blockNumber BigInt
txHash String
from String
to String
wallet String
direction String // "in" | "out"
value String // stored as string; BigInt serialization
timestamp BigInt
token String? // ERC20 only
logIndex Int? // ERC20 only
createdAt DateTime @default(now())
}Then run:
npx prisma db push
npx prisma generatearc-feed/
├── docker-compose.yml
├── prisma.config.ts
├── package.json
├── tsconfig.json
├── .env.example
├── prisma/
│ └── schema.prisma # Wallet model (extend here)
└── src/
├── index.ts # Entry point — wires config, db, chain
├── config.ts # Environment variable parsing
├── types.ts # AppConfig type
├── chain/
│ ├── client.ts # viem PublicClient + Arc chain config
│ ├── watcher.ts # AsyncGenerator block polling & backfill
│ └── processor.ts # Per-block orchestration (2 RPC calls)
├── db/
│ ├── client.ts # Prisma singleton
│ └── wallets.ts # Wallet load query
├── parsers/
│ ├── types.ts # Transfer interface & ParsedBlock
│ ├── native.ts # Native transfer matching
│ └── erc20.ts # ERC20 Transfer event matching
└── lib/
├── logger.ts # Structured JSON logger
└── output.ts # Colorized console output
Arc-native — hardcoded to Arc via viem's arcTestnet chain definition. Native currency symbol and decimals are chain-aware. This is not a generic EVM parser with an Arc config option — it is built specifically for Arc's block structure and finality model.
No confirmation depth — Arc's Malachite BFT consensus delivers deterministic finality at the block level. There is no "wait N blocks" logic in Arc Feed because Arc does not require it. One committed block is final.
viem over ethers.js — smaller bundle, tree-shakable, native batched JSON-RPC support, TypeScript-first.
Prisma 7 + driver adapter — @prisma/adapter-pg driver adapter pattern gives type-safe database access without Prisma's binary engine overhead. Easy to extend with new models.
Unfiltered getLogs — fetches all Transfer events for the block, filters in-memory. Keeps RPC calls at exactly 2 per block regardless of how many wallets or how many token contracts you care about.
AsyncGenerator for backpressure — block watching uses async function*. The processor pulls blocks from the generator; there is no internal buffer that can grow unboundedly under backpressure.
Arc Feed is open source under the Apache 2.0 license. Contributions welcome.
Run checks before submitting a PR:
npm run typecheck # TypeScript type checking
npm run lint # ESLint
npm run format:check # PrettierCommits follow Conventional Commits (enforced by commitlint + husky).
Open an issue before starting work on a large change — it helps align on direction before you invest time.
Apache 2.0 — see LICENSE.
Copyright 2026 Devancore™