--- title: ScannerBalanceWatch (Scanner DB model) tags: [data-model, scanner, payment] created: 2026-06-03 --- # ScannerBalanceWatch SQLite row in the AMN Pay Scanner's `balance_watches` table. One row represents a direct-address token balance watch requested by the backend for non-smart-contract payment detection. This is scanner-internal state. It is not a Mongoose model and lives in the scanner SQLite database (`/data/scanner.db`). --- ## Schema ```sql CREATE TABLE balance_watches ( watch_id TEXT PRIMARY KEY, chain_id INTEGER NOT NULL, chain_type TEXT NOT NULL DEFAULT 'evm', token_address TEXT NOT NULL, token_symbol TEXT, decimals INTEGER NOT NULL DEFAULT 0, address TEXT NOT NULL, baseline_balance TEXT NOT NULL, current_balance TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'watching', callback_url TEXT NOT NULL, callback_secret TEXT NOT NULL, last_checked_at DATETIME, next_check_at DATETIME NOT NULL, change_count INTEGER NOT NULL DEFAULT 0, last_notified_at DATETIME, expires_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_balance_watches_status_next ON balance_watches(status, next_check_at); CREATE INDEX idx_balance_watches_chain_status ON balance_watches(chain_id, status); ``` --- ## Fields | Field | Type | Description | |---|---|---| | `watch_id` | TEXT PK | Caller-supplied or scanner-generated idempotency key. Backend should use a payment-scoped value such as `-balance-c56-USDT` when it wants webhook correlation by prefix. | | `chain_id` | INTEGER | Numeric EVM chain ID. Direct balance reads currently support EVM ERC-20 only. | | `chain_type` | TEXT | Currently `evm` for balance watches. Kept for future Tron/TON support. | | `token_address` | TEXT | ERC-20 token contract address, normalized to lowercase `0x` hex. | | `token_symbol` | TEXT NULL | Optional token symbol resolved from `tokens.json`. | | `decimals` | INTEGER | Token decimals resolved from registry when available. | | `address` | TEXT | Watched holder address, normalized to lowercase `0x` hex. | | `baseline_balance` | TEXT | Base-unit integer string captured or supplied when the watch is created. | | `current_balance` | TEXT | Last scanner-accepted base-unit balance. For changed balances, this advances only after webhook delivery succeeds. | | `status` | TEXT | Watch lifecycle state. | | `callback_url` | TEXT | Backend webhook endpoint. Validated with the same callback URL guard as scanner intents. | | `callback_secret` | TEXT | HMAC-SHA256 key for the balance watch webhook signature. Never returned in API responses. | | `last_checked_at` | DATETIME NULL | Last time the scanner attempted a balance read. | | `next_check_at` | DATETIME | Scheduler due time. | | `change_count` | INTEGER | Count of successfully delivered balance-change notifications. | | `last_notified_at` | DATETIME NULL | Time of the last successful balance-change notification. | | `expires_at` | DATETIME | Hard stop timestamp, currently 7 days after creation. | | `created_at` / `updated_at` | DATETIME | UTC timestamps. | --- ## Status values | Status | Description | |---|---| | `watching` | Scheduler polls the address/token when `next_check_at` is due. | | `stopped` | Backend explicitly stopped the watch after payment success, cancellation, or manual resolution. | | `expired` | Scanner stopped the watch automatically after 7 days. | --- ## Polling cadence | Age from `created_at` | Interval | |---|---| | `< 24h` | 5 min | | `24h–48h` | 10 min | | `48h–72h` | 20 min | | `> 72h` | 40 min until expiry | `BALANCE_WATCH_TICK_SEC` controls how often the scheduler queries for due watches. `BALANCE_WATCH_BATCH_SIZE` controls how many due watches are processed per tick. --- ## Webhook semantics When `current_balance` changes, scanner sends: ```json { "eventType": "balance_changed", "watchId": "6840fabc-balance-c56-USDT", "chainId": 56, "chainType": "evm", "address": "0x...", "tokenAddress": "0x...", "tokenSymbol": "USDT", "decimals": 18, "previousBalance": "25000000000000000000", "currentBalance": "35000000000000000000", "delta": "10000000000000000000", "changeCount": 1, "checkedAt": "2026-06-03T10:05:00Z", "status": "balance_changed" } ``` The backend must not treat this webhook alone as final escrow funding. It should compare `delta` or `(currentBalance - baselineBalance)` to the expected amount, apply token/chain/address checks, persist evidence, and stop the watch when the payment is accepted or cancelled. --- ## Related - [Scanner Architecture](../01%20-%20Architecture/Scanner%20Architecture.md) - [Scanner API](../03%20-%20API%20Reference/Scanner%20API.md) - [Payment Flow - Scanner](../04%20-%20Flows/Payment%20Flow%20-%20Scanner.md) - [ScannerIntent](ScannerIntent.md) - [Payment](Payment.md) — backend MongoDB/DTO model that stores payment metadata