The monitor exposes a small read-oriented HTTP API. All responses are JSON, and every path below is relative to the
host serving this page (shown as https://your-host in the examples).
Read endpoints are unauthenticated. The two write endpoints (/api/subscribe, /api/unsubscribe)
require a shared secret, compared in constant time. Timestamps are unix milliseconds throughout.
Liveness probe. Always returns 200 while the process is up.
# request curl https://your-host/api/healthz # response { "ok": true }
The full current state powering the dashboard: every observed (exchange, token, network) row,
per-exchange poll freshness, active subscriptions, the 24h change count, and the market-cap ranking used to
order the token matrix. The dashboard polls this every 30s.
| Field | Type | Description |
|---|---|---|
rows | array | Current state rows (see shape below). One per (exchange, token, network). |
lastPolledByExchange | object | Map of exchange id → unix-ms of its last successful poll, or null if it has never completed one. |
subscriptions | array | Active subscriptions: { token, createdAt, note }. |
changeCount24h | number | Count of state-change events in the last 24 hours. |
topSymbols | string[] | Tokens in market-cap-rank order (drives matrix sorting / implicit top-100). |
topSymbolRank | object | Map of token → market-cap rank (1 = largest). |
now | number | Server clock (unix-ms) at response time, for freshness math. |
# response (truncated)
{
"rows": [
{
"exchange": "binance", "token": "USDT", "network": "TRC20",
"deposit": 1, "withdraw": 0,
"depositReason": null, "withdrawReason": "Wallet Maintenance",
"firstSeen": 1747000000000,
"depositChangedAt": null, "withdrawChangedAt": 1747200000000
}
],
"lastPolledByExchange": { "binance": 1747300000000, "bybit": null },
"subscriptions": [ { "token": "USDC", "createdAt": 1747100000000, "note": "depeg watch" } ],
"changeCount24h": 42,
"topSymbols": [ "BTC", "ETH", "USDT" ],
"topSymbolRank": { "BTC": 1, "ETH": 2, "USDT": 3 },
"now": 1747300001000
}
deposit/withdraw are 1 (enabled) or 0 (disabled).
A *ChangedAt of null means no flip has been observed since firstSeen — the UI shows
"Observed since" rather than a confirmed "Changed" timestamp.
The append-only history of status flips, newest first. Every deposit/withdraw change is one row, tagged with the exchange it came from. History is retained for 365 days.
| Param | Type | Default | Description |
|---|---|---|---|
token | string | all | Filter to one token symbol, e.g. USDT. |
field | string | all | Filter by change type: deposit or withdraw. Any other value is ignored. |
since | number | — | Lower bound, inclusive: only rows with ts ≥ since (unix-ms). |
before | number | — | Upper bound, exclusive: only rows with ts < before (unix-ms). Use for backward paging. |
limit | number | 100 | Max rows returned. Capped at 1000. |
| Field | Type | Description |
|---|---|---|
id | number | Auto-increment row id. |
ts | number | When the change was observed (unix-ms). |
exchange | string | Exchange id, e.g. binance. |
token | string | Token symbol. |
network | string | Network identifier as reported by the exchange. |
field | string | deposit or withdraw. |
oldValue / newValue | number | 0 = disabled, 1 = enabled. |
reason | string? | Reason text if the exchange provides one (Binance/MEXC), else null. |
# request curl "https://your-host/api/changes?token=USDT&field=withdraw&limit=500" # response { "rows": [ { "id": 8123, "ts": 1747200000000, "exchange": "binance", "token": "USDT", "network": "TRC20", "field": "withdraw", "oldValue": 1, "newValue": 0, "reason": "Wallet Maintenance" } ] }
ts in a page and pass it as before on the next request:
?before=1747200000000&limit=1000. Repeat until a page returns fewer than limit rows.
Lists active token subscriptions. Each subscribed token's future state changes are pushed to the configured market-alerts webhook.
# response
{
"subscriptions": [
{ "token": "USDC", "createdAt": 1747100000000, "note": "depeg watch" }
]
}
Adds (or updates the note on) a token subscription. Token is normalized to upper-case and trimmed. Returns the full updated list.
| Field | Type | Description | |
|---|---|---|---|
token | string | required | Token symbol to subscribe. |
secret | string | required | The shared secret required for write endpoints. |
note | string | Optional free-text note shown in the subscriptions panel. |
| Status | Meaning |
|---|---|
200 | Subscribed. Body is { subscriptions: [...] }. |
400 | { error: "token required" } — empty/missing token. |
401 | Missing or wrong secret. Empty body. |
curl -X POST https://your-host/api/subscribe \
-H "Content-Type: application/json" \
-d '{ "token": "USDC", "note": "depeg watch", "secret": "<shared-secret>" }'
Removes a token subscription. Returns the full updated list.
| Field | Type | Description | |
|---|---|---|---|
token | string | required | Token symbol to unsubscribe. |
secret | string | required | The shared secret required for write endpoints. |
| Status | Meaning |
|---|---|
200 | Removed (idempotent — 200 even if it wasn't subscribed). Body is { subscriptions: [...] }. |
400 | { error: "token required" } — empty/missing token. |
401 | Missing or wrong secret. Empty body. |
curl -X POST https://your-host/api/unsubscribe \
-H "Content-Type: application/json" \
-d '{ "token": "USDC", "secret": "<shared-secret>" }'
Recent webhook delivery results for each dispatcher — one for market alerts, one for health alerts — for debugging. If both dispatchers are configured to share a single webhook, the two fields describe the same one. The dashboard surfaces a warning banner when either dispatcher has 3+ consecutive delivery failures.
# response
{
"market": { "consecutiveFailures": 0, "recent": [ { "ts": 1747300000000, "ok": true, "status": 200 } ] },
"health": { "consecutiveFailures": 0, "recent": [ ... ] }
}