How it works
Reliability outside your business logic.
The watcher advances through confirmed blocks in bounded ranges. It saves both the block number and hash only after every matching record succeeds. Restarts resume at the next block; a changed hash triggers a configurable rewind and replay.
Provider agnostic
Use ethers, viem, or a custom RPC client. Only block number and block lookup methods are required.
At-least-once
Failures replay safely instead of skipping work. A stable event id helps handlers enforce idempotency.
Production controls
Confirmation lag, maximum block range, reorg rewind, immediate startup scans, and serialized runs are built in.
Quick start
Four adapters, one durable loop.
import { createChainWatcher } from "@vowlabs/chain-watcher";
const watcher = createChainWatcher({
id: "merchant-payments.ethereum",
provider,
checkpointStore,
confirmations: 2,
blockRange: 500,
scan: ({ fromBlock, toBlock }) => findPayments(fromBlock, toBlock),
handle: (payment, { eventId }) => applyOnce(eventId, payment)
});
await watcher.start();
Delivery contract
Make handlers idempotent.
A handler can complete before a later record in the same range fails. The whole range will be retried, so store the supplied event id under a unique constraint or transactionally verify current business state before applying a side effect.
PriceEdge is the reference implementation: its adapter discovers ERC-20 transfers to pending invoice addresses and updates an invoice only while it remains pending.
Container
Run it with Docker.
The repository includes a minimal container that watches standard JSON-RPC logs, streams matches as JSON lines, and persists its block checkpoint in a mounted data volume.
docker build -t vowlabs-chain-watcher .
docker run --rm \
-e RPC_URL=https://your-rpc.example \
-e WATCH_ADDRESS=0xYourContractAddress \
-e 'WATCH_TOPICS=["0xYourEventTopic"]' \
-v chain-watcher-data:/data \
vowlabs-chain-watcher
VLBurn observer
Reconcile plain ERC-20 arrivals.
ERC-20 transfers do not carry callback data, so tokens sent directly to VLBurn remain inert. The included vlburn-observer.mjs watches configured wrapper transfers, waits for confirmation depth, and submits each arrival through an authorized observer account.
Each record is keyed by the transaction hash and log index, allowing several transfers from one transaction to be processed independently. Burn-only tokens use post-burn handler ID 0; burn-for-target tokens provide their registered handler ID through POST_BURN_IDS.
RPC_URL=https://your-rpc.example \
OBSERVER_PRIVATE_KEY=0x... \
VLBURN_ADDRESS=0x... \
TOKEN_ADDRESSES=0xWrapperA,0xWrapperB \
POST_BURN_IDS=0,3 \
node examples/vlburn-observer.mjs