Files
nick-doc/02 - Data Models/ScannerBalanceWatch.md

5.1 KiB
Raw Blame History

title, tags, created
title tags created
ScannerBalanceWatch (Scanner DB model)
data-model
scanner
payment
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

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:

{
  "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.