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:
- Fetch the latest block number.
- For each new block since the last indexed block, fetch the full block with transactions.
- Filter transactions to those where a watched wallet is
fromorto. - For each relevant transaction:
- Fetch the receipt (to get
gasUsedandstatus). - Decode the calldata (see Transaction Decoder).
- Write to
transactionstable. - Trigger the webhook notifier.
- Fetch the wallet's current ETH balance and cache it.
- Run anomaly detection.
- Fetch the receipt (to get
- Update
indexer_statewith 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:
- Converts
valueWeito ETH. - Queries for all webhooks where
walletAddressmatches the transaction sender or recipient, andthresholdEthis below the transaction value. - For each matching webhook, POSTs the alert payload.
- On failure, waits 5 seconds and retries once.
- Records the delivery attempt (success or failure) to the
webhook_deliveriestable.
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.