0watch Architecture

0watch has three components: a block indexer, a REST API, and a webhook notifier. They share a SQLite database and run as separate processes.


Components

Base RPC
    │
    ▼
┌─────────────────┐
│  Block Watcher  │  polls every 2s for new blocks
│  (indexer)      │  filters txs for watched wallets
└────────┬────────┘
         │ writes
         ▼
┌─────────────────┐       ┌──────────────────────┐
│  SQLite DB      │◄──────│  REST API (port 3001) │◄── HTTP clients
│  data/0watch.db │       └──────────────────────┘
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  Webhook        │  fires on high-value transactions
│  Notifier       │  HTTP POST or Telegram message
└─────────────────┘

Block Watcher

src/services/indexer/watcher.ts

The block watcher polls Base mainnet via JSON-RPC. On each poll:

  1. Fetch the latest block number.
  2. For each new block since the last indexed block, fetch the full block with transactions.
  3. Filter transactions to those where a watched wallet is from or to.
  4. For each relevant transaction:
    • Fetch the receipt (to get gasUsed and status).
    • Decode the calldata (see Transaction Decoder).
    • Write to transactions table.
    • Trigger the webhook notifier.
    • Fetch the wallet's current ETH balance and cache it.
    • Run anomaly detection.
  5. Update indexer_state with the last indexed block number.

On RPC error or rate limit, the watcher logs the error, saves progress to the last successful block, and resumes on the next poll cycle.


Transaction Decoder

src/services/indexer/decoder.ts

Decodes raw transaction calldata into typed records. Recognized patterns:

Type Detection
eth_transfer to is set, no calldata, value > 0
erc20_transfer calldata matches transfer(address,uint256) selector
erc20_approval calldata matches approve(address,uint256) selector
uniswap_swap calldata matches Uniswap V2/V3/Universal Router selectors
unknown anything else

Anomaly Detection

src/services/indexer/anomaly.ts

Runs synchronously after each transaction is indexed. Three detectors:

failed_tx — fires when receipt.status === 0 (transaction reverted). Severity scales with how often the wallet has recent failures.

large_transfer — fires when a single ETH transfer exceeds largeTransferThresholdPct percent of the wallet's current balance. Severity scales with the percentage.

high_velocity — fires when total ETH outflow across the last velocityWindowBlocks blocks exceeds velocityThresholdEth. Severity scales with how far over the threshold the outflow is.

Anomaly records are written to the anomalies table and are queryable via GET /api/wallets/:address/alerts.


Webhook Notifier

src/services/indexer/notifier.ts

Runs inline with the indexer — not a separate process. After each transaction is written to the database, the notifier:

  1. Converts valueWei to ETH.
  2. Queries for all webhooks where walletAddress matches the transaction sender or recipient, and thresholdEth is below the transaction value.
  3. For each matching webhook, POSTs the alert payload.
  4. On failure, waits 5 seconds and retries once.
  5. Records the delivery attempt (success or failure) to the webhook_deliveries table.

Telegram webhooks go through the Telegram Bot API (sendMessage) rather than a direct HTTP POST.


Database

SQLite at data/0watch.db (configurable via DB_PATH env var). Schema is applied automatically on startup.

Table Contents
wallets Registered addresses and labels
transactions Indexed transactions with decoded calldata
anomalies Detected anomaly events
balances Latest ETH balance per wallet
indexer_state Last indexed block number and status
webhooks Registered webhook endpoints
webhook_deliveries Delivery history (success and failure)

Configuration

The indexer accepts a partial IndexerConfig object. All fields have defaults.

Field Default Description
chainId 8453 Chain ID. Only Base mainnet is supported.
rpcUrl https://base.drpc.org JSON-RPC endpoint.
dbPath data/0watch.db SQLite database path.
pollIntervalMs 2000 Block polling interval in milliseconds.
velocityWindowBlocks 10 Block window for velocity anomaly detection.
velocityThresholdEth 1.0 ETH outflow that triggers a high-velocity alert.
largeTransferThresholdPct 50 Percent of balance that triggers a large-transfer alert.

API environment variables:

Variable Default Description
API_PORT 3001 Port the REST API listens on.
DB_PATH data/0watch.db Path to the SQLite database (shared with indexer).

Programmatic usage

You can embed 0watch in your own Node.js process instead of running separate services:

import { ZeroWatch } from './src/services/indexer/index.js';

const watch = new ZeroWatch(
  {
    rpcUrl: 'https://mainnet.base.org',
    velocityThresholdEth: 0.5,
    largeTransferThresholdPct: 25,
  },
  'configs/watched-addresses.json'
);

// Or register wallets at runtime
watch.registerWallet('0xabcd...', 'treasury');

watch.start();

// Query indexed data
const txs = watch.getTransactions('0xabcd...');
const anomalies = watch.getAnomalies('0xabcd...');
const balance = watch.getEthBalance('0xabcd...');

await watch.stop();

The ZeroWatch class manages the database, block watcher, and webhook notifier as a unit.