docs: sync from backend 22ae0bd — scanner balance watches

This commit is contained in:
Siavash Sameni
2026-06-03 21:23:50 +04:00
parent 4b1d8ea36d
commit 9dcdb420fc
9 changed files with 866 additions and 13 deletions

View File

@@ -0,0 +1,130 @@
---
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 `<paymentId>-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 |
| `24h48h` | 10 min |
| `48h72h` | 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

View File

@@ -111,4 +111,5 @@ A backfill pass recomputes `topic_ref` for existing EVM intents that had it as N
- [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)
- [ScannerBalanceWatch](ScannerBalanceWatch.md) — direct-address balance-watch model for non-smart-contract payment rails
- [Payment](Payment.md) — the backend MongoDB model that triggers intent creation