Some nests need more than raw rows: screen addresses against a sanctions list, flag transfers that cross a threshold, raise an alert when something fires. The compliance pack is an optional stage over your decoded transfers - off by default, deterministic, and, like every optional integration, it costs nothing when it isn’t configured.
Screening lists are local, content-addressed snapshots you fetch and pin - nuthatch never reaches for a gated screening service mid-decode. That would break the no-phone-home rule.
The shape
[screening]
lists = ["<list-hash>"] # content-addressed snapshots from `nuthatch lists fetch`
[flags] # singular table - two built-in rules
threshold = "1000000000000" # flag any single transfer ≥ this (base units, decimal string)
velocity_amount = "5000000000000" # flag an address whose windowed outbound volume ≥ this
velocity_window = 7200 # window in BLOCKS (≈24h at 12s blocks); default 7200
[[alerts]]
kinds = ["threshold_flag", "sanction_hit"] # which annotation kinds to deliver
url = "https://your-service/alerts"
[screening]- screen each transfer’sfrom/toagainst pinned list snapshots. A match becomes an append-onlysanction_hitannotation, stamped with the list-snapshot version it matched. Fetch a list first withnuthatch lists fetch(built-inofac-sdn,eu-consolidated, or your own via--url/--file).[flags]- two built-in rules, not arbitrary SQL.thresholdflags any single transfer at or above an amount; each match becomes a sealed, append-onlythreshold_flagannotation.velocityflags an address whose outbound volume overvelocity_windowblocks reachesvelocity_amount; this one is a live windowed view (served at/flags?kind=velocity), not a sealed annotation - so it isn’t deliverable via alerts. Amounts are base units as decimal strings; the window is a block count, not wall-clock - an honest approximation, since the chain has no clock.[[alerts]]- deliver annotations whosekindis inkindsto aurl. The only valid kinds arethreshold_flagandsanction_hit- they match the emitted annotations exactly.
Determinism holds
Screening and flags are deterministic derivations over decoded transfers - a list-snapshot match and two
numeric rules, nothing that phones out mid-decode. They’re re-executable: the same blocks and the same
list snapshot always produce the same annotations, which is what makes the signed audit pack
(nuthatch audit replay) able to re-prove them from scratch. Effectful notification (the alert POST)
happens after sealing, never in the decode path - the same rule as webhooks and enrichers (see
Determinism).
Delivery is durable
Alerts ride the same host-side webhook delivery engine: at-least-once, via a
durable outbox, with retries and backoff. A stalled sink never blocks indexing - it only backs up its own
queue (nuthatch_alert_outbox_depth in /metrics).
Query the annotations
Flags and hits are ordinary rows. Query the live counts over the HTTP API - GET /flags?kind=threshold
or ?kind=velocity - or the full sealed history in SQL:
SELECT * FROM sanction_hit ORDER BY block_number DESC LIMIT 20;
Next
- Webhooks - the delivery engine alerts share
- The semantic layer - describe what a flag means
- nuthatch.toml - where the stage is configured