Monitor
A monitor is the rule that ties the other three kinds together: an id, the
network it watches, one or more selectors, an optional predicate, an
optional gate, and actions naming the sinks it delivers to (Monitor,
crates/blockwatcher-types/src/resource.rs). It is written through
PUT /monitors/{id} on the HTTP API or as one JSON file
under a seed directory’s monitors/ subdirectory. Writing a monitor usually
restarts nothing: the running pipeline hot-swaps the recompiled monitor set in
place. Two things restart that one network instead: changing gate, whose
previous envelope’s holds are dropped with the pipeline stopped and the
pipeline then brought back (deleting a gated monitor takes the same path),
and naming a sink the pipeline never spawned a worker for, per
Resources § Lifecycle.
Dropping holds never drops an already-emitted digest: an emission the old
envelope committed but had not yet delivered survives the wipe and is
delivered (or dead-lettered) when the pipeline comes back, see
Gates § Delivery guarantee.
Monitor
id MonitorId
network NetworkId
selectors RawSelector[]
predicate string? // this monitor's filter, not a shared id
gate ModuleSel? // this monitor's decision rule, like predicate
actions SinkId[]
A complete monitor resource, selecting against the erc20 spec from the
Spec page:
{
"id": "usdc-transfers",
"network": "eth-mainnet",
"selectors": [
{
"spec": "erc20",
"events": ["Transfer"],
"addresses": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]
}
],
"predicate": "args.value > 1000000",
"actions": ["alerts"]
}
The same monitor with a threshold gate; omitting gate (the first
example) remains valid passthrough:
{
"id": "usdc-burst",
"network": "eth-mainnet",
"selectors": [
{
"spec": "erc20",
"events": ["Transfer"],
"addresses": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]
}
],
"predicate": "args.value > 1000000",
"gate": {
"module": "threshold",
"config": { "count": 3, "window_ms": 3600000 }
},
"actions": ["alerts"]
}
Fields
Monitor rejects an unrecognized top-level field
(#[serde(deny_unknown_fields)]); a selector entry’s keys are the one
exception, checked later by the chain’s decoder rather than by serde, per
the selector table below.
| Key | Type | Default | Meaning |
|---|---|---|---|
id | string | none (required) | The monitor’s name; every delivered match carries it. |
network | string | none (required) | The network this monitor watches. The write refuses an id that does not exist. Fixed at creation: an update naming a different network is refused (422); delete and recreate the monitor to move it. |
selectors | array of objects | none (required) | What to decode and match, OR’d across entries. An empty array refuses: a monitor needs at least one selector. |
predicate | string | absent | An expression over the decoded event; absent matches everything the selectors decode. |
gate | object | absent | { "module", "config" }. Absent = passthrough. See Gates. |
actions | array of strings | none (required) | The sinks to deliver each match to, one or more. The write refuses an id that does not name an existing sink. |
The whole monitor (selectors, predicate, and gate) must compile against the live
decoder, matcher, and gate catalog before the write is persisted, so every refusal quoted
below surfaces as a 422 on the write, per
Resources § Write-time validation.
Selector keys
Each selector entry carries a core-readable spec reference plus
decoder-owned keys. For the evm decoder those are events, functions,
and addresses, all optional; any other key refuses by name at compile
time: unknown selector key '<key>' (compile,
crates/blockwatcher-evm/src/decoder/selector.rs).
| Key | Type | Default | Meaning |
|---|---|---|---|
spec | string | none (required) | The spec this entry decodes against. A missing key refuses: selector requires a 'spec' reference. The spec must exist and share the network’s chain. |
events | array of strings | absent | Event names to select from the spec, all overloads of each name. Absence and emptiness mean opposite things, per the rule below. |
functions | array of strings | absent | Function names to select from the spec, same shape as events. |
addresses | array of strings | absent | Contract addresses this entry is restricted to; absent means any address. Each entry restricts only the events and functions it names, never a sibling entry’s. |
The absent-vs-empty rule
events and functions share one presence matrix:
- Naming neither selects every event AND every function the spec declares,
ABI-scoped rather than chain-wide, mirroring how an absent
addressesmeans any address. - Naming either selects only what it names; the omitted kind is left unselected.
- An explicit empty array is refused rather than treated as absence, because
the two spellings mean opposite things and the empty one would compile an
entry that can never fire:
selector's 'events' is empty: omit the key to select every declaration of that kind in the spec, or name at least one(functionsandaddressesrefuse with the same shape; theaddressesmessage endsomit the key to match any address, or name at least one).
What else a selector refuses
- A non-string entry is refused by position, never silently dropped:
selector's 'events' entry at position 1 is not a string; every entry must be a name. - A name the spec does not declare is refused with a suggestion, the spec’s
first declared event or function of that kind:
unknown field 'Transfr'plusdid you mean 'Approval'?(see Resources § The did-you-mean suggestion). - An address must be a
0x-prefixed 40-hex-digit string:address '<raw>' must be a 0x-prefixed 40-hex-digit string. - A mixed-case address claims an EIP-55 checksum and is held to it:
address '<raw>' is mixed-case, which claims an EIP-55 checksum, but the checksum does not match; check for a mistyped character. An all-lowercase or all-uppercase spelling makes no checksum claim and is accepted as-is.
What a selected event or function actually decodes to, and which raw material reaches the selector per source, is documented on Selectors.
Predicate
predicate is one expression string, compiled at write time against the
schemas the monitor’s selectors produce; a field or namespace it names that
no selected schema declares refuses the write, with a bounded edit-distance
suggestion. The language (syntax, operators, the three-valued evaluation,
and the type families) is documented on
Predicates and the expression language.
Gate
gate compiles at write against the same schemas as the predicate.
Refusals (all 422):
- unknown field on the envelope or inside
config(deny_unknown_fields) - unknown
module(message lists catalog gate names) window_ms0or> 86400000: same bound language as sinkthrottle.window_msthreshold.count< 2or> 10000- missing
block.timestampon the schema:gate requires 'block.timestamp'; this monitor's selectors do not expose it
Changing module or config drops persisted holds for that monitor; they
are not migrated.