Skip to content

Devancore/arc-feed

Repository files navigation

Devancore Arc Feed

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.


Why This Tool

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.


Quick Start

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 start

How It Works

Arc 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 — getLogs fetches 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

Configuration

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

Transport: HTTP vs WebSocket

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.


Wallet Management

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.


Transfer Detection

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 from and to are tracked wallets, two transfers are emitted — one IN and one OUT

Backfill

To process historical blocks before starting live monitoring, set START_BLOCK:

START_BLOCK=30264990

Arc 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

Logging

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.


What You Can Build

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.


Extending the Schema

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 generate

Project Structure

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

Design Decisions

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.


Contributing

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 # Prettier

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


License

Apache 2.0 — see LICENSE.

Copyright 2026 Devancore™

About

Real-time wallet transfer monitor for Arc Network. Built on deterministic finality. Self-hosted, no indexing dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors