Skip to main content

Monitoring endpoints

Two routes let an external scheduler watch the deployment:

RouteJob
GET /api/ops/checkRun every check, remember each check's state, and send one alert when a state changes
GET /api/ops/reportBuild the monthly service report from the stored history, and optionally post it once

Both are read only. They never send a transaction, never sign anything, and their answers name states and contract addresses only, never a person's wallet, a document, a key or an endpoint address.

The operating procedure (what to do when an alert arrives) is in Runbooks and Monitoring flow.

Off until configured, as of 2026-10-24

The monitor is built but not running on the Sepolia deployment. It needs CRON_SECRET, a shared Redis store, an alert channel (OPS_WEBHOOK_URL) and a schedule. The repository holds no scheduler configuration (no vercel.json cron entry), so the operator sets up the scheduler. The probe check also needs a canary credential and the OPS_PROBE_* names.

Calling the routes​

GET /api/ops/check HTTP/1.1
Authorization: Bearer <CRON_SECRET>
StepFailure
Method is GET405 BAD_REQUEST "Use GET." with Allow: GET
CRON_SECRET is set and at least 32 characters401 UNAUTHORIZED "Not allowed." and a server log line
The bearer value equals the secret (constant-time comparison)401 "Not allowed."
A store is available500 CONFIG_MISSING "The monitor is not configured."
At most 6 calls per 60 seconds429 with Retry-After
The run completes500 INTERNAL_ERROR "The monitor could not run."

The secret is checked before anything else, so a caller without it learns nothing about the setup and cannot use up the limit. The limit stops a leaked secret from being used to flood the RPC providers. Both routes run on Node.js with maxDuration = 60 seconds.

A successful check run answers:

{
"data": {
"results": [
{ "name": "rpc", "state": "ok", "detail": "2 of 2 endpoints answer on chain 11155111." },
{ "name": "governance", "state": "ok", "detail": "6 registries have the same admin and no pending transfer." }
],
"alertSent": null
}
}

Each check writes a fixed phrase with counts and contract labels; the numbers above are an example. alertSent is null when nothing changed, true when an alert went out, and false when one was due but there is no channel or sending failed.

The checks​

buildOpsChecks (src/lib/server/ops/wiring.ts) builds the list from the settings:

CheckWhat it looks atWarnFail
rpcEvery configured RPC endpoint, 5 seconds eachAn endpoint is more than OPS_MAX_LAG_BLOCKS (default 20) behind the others, or did not answer while another didAn endpoint is on another chain, or none answered
chain-eventsAdmin events over the last OPS_LOOKBACK_BLOCKS (default 600, max 50,000) in every listed set and the timelockAny watched event happened (up to 5 listed)The logs could not be read
governanceThe admin of every registry and any pending admin transferA transfer is pending, the registries have different admins, or a registry is not administered by the manifest's timelock (when one is recorded)The admins could not be read
pinsRetries failed second pins, then reads back the 10 newest pins through each gateway and compares their SHA-256A gateway could not serve a pin, or copies are still waiting for repairA gateway returned different bytes, or no gateway could serve a pin
signersThe addresses of STATUS_LIST_SIGNER_KEY and API_SIGNER_KEY against their published addressesA key without a published address, or the reverseA key does not match its published address
probeFetches the canary credential through the public API, checks the signed envelope against the pinned signer, and times itVerified but took 2 seconds or moreNo answer, a status other than 200, not signed by the pinned signer, outside its window, nonce not echoed, or the canary not in good standing
keysExpiry dates in OPS_KEY_EXPIRIESWithin OPS_WARN_DAYS (default 14)Past
configPresent only when a setting is malformed or the chain settings are missingAlways

pins exists only when a second pin provider is configured, and probe only when the OPS_PROBE_* names are set. A check that throws becomes a fail with "The check could not run." A check that cannot be built becomes "The <name> check cannot be set up. See the server log."

The watched events are:

ContractEvents
IssuerRegistryPaused, Unpaused, IssuerAdded, IssuerRemoved, IssuerRetired, RoleGranted, RoleRevoked, DefaultAdminTransferScheduled, DefaultAdminTransferCanceled, DefaultAdminDelayChangeScheduled
TimelockCallScheduled, CallExecuted, Cancelled, MinDelayChange, RoleGranted, RoleRevoked
StatusRegistryIssuerSuspensionChanged, RoleGranted, RoleRevoked, DefaultAdminTransferScheduled, DefaultAdminTransferCanceled

An event is not necessarily a problem: a planned issuer registration also shows. The warning makes sure somebody looks.

The probe in detail​

The probe (checks/probe.ts) calls GET {OPS_PROBE_API_URL}/api/v1/credentials/{OPS_PROBE_CANARY_ID} with a fresh nonce, optionally with OPS_PROBE_API_KEY, and a 10 second timeout. It then:

  • checks the envelope with checkEnvelope against NEXT_PUBLIC_API_SIGNER, allowing 60 seconds of clock difference;
  • requires the canary to be found, active, not expired and from a registered, unsuspended issuer;
  • records the time taken, and how far behind the chain the answer could be: the gap between generatedAt and the timestamp of the block it read, plus dataMaxAgeSeconds.

These numbers feed objectives S1 to S3 below.

Alerts​

src/lib/server/ops/monitor.ts keeps each check's last state in the store (ops:state:<name>, 30 days). After a run it compares:

  • A change to warn or fail, or a recovery from them to ok, is put in one alert listing every change.
  • ok after ok, or the first ok ever, sends nothing.
  • A new state is remembered only after the alert was sent, so a failed send is retried on the next run.

src/lib/server/ops/alerts.ts posts { "text": ..., "content": ... } (the same text for Slack-style and Discord-style webhooks) to OPS_WEBHOOK_URL. The address must be https; redirects are refused; the timeout is 5 seconds; each detail is cut to 200 characters and the whole text to 1,400.

History and the monthly report​

Each run adds to a daily record in the store (slo:<day>, kept 400 days): per-check counts of ok, warn and fail, and probe counts (runs, good, fast, measured staleness, within 5 minutes). Alerts of the month are kept under slo:alerts:<month>. Without a shared store (Redis) there is no durable history.

GET /api/ops/report takes:

QueryMeaning
month=YYYY-MMThis month or one of the twelve before it; default the previous month. Other values: 400 "The month must be YYYY-MM, this month or one of the twelve before it."
post=1Post the report to the webhook once per month. Not for an unfinished month. A failed post answers 502 "The report could not be posted. Try again."

The report measures six objectives (src/lib/server/ops/slo/report.ts):

IdObjectiveTargetMeasured from
S1Verification answers99.5% of probesProbes that returned a good, signed answer
S2Revocation visible95% within 5 minutesProbes whose answer was at most 300 seconds behind the chain
S3Answer speed95% under 2 secondsGood probes faster than 2,000 ms
S4Documents retrievable99% of runspins runs that did not fail
S5Chain reads99.5% of runsrpc runs that did not fail
S6Support responsenoneReported from the support log, not by the monitor

Percentages are rounded down. A warning counts as available, because it means one of several providers had a problem. The report states what is not subtracted from the figures: the chain itself halting, a partner's own wallet or network, and maintenance announced three working days ahead.

Targets are proposals

The targets are proposals for pilot partners (P5-S05) and have not yet been agreed or measured over a month.

Configuration​

NameMeaning
CRON_SECRETBearer secret for the scheduler, at least 32 characters
OPS_WEBHOOK_URLhttps webhook for alerts and reports
OPS_MAX_LAG_BLOCKS, OPS_LOOKBACK_BLOCKS, OPS_WARN_DAYSThresholds (defaults 20, 600, 14)
OPS_KEY_EXPIRIESJSON object of key names to YYYY-MM-DD dates
OPS_PROBE_API_URL, OPS_PROBE_CANARY_ID, OPS_PROBE_API_KEYThe probe; setting only some of url and canary is a configuration error
NEXT_PUBLIC_API_SIGNERThe signer the probe pins
NEXT_PUBLIC_NETWORK_ID, RPC namesThe chain
Redis namesStore for state, history and limits

Source files​

  • src/app/api/ops/check/route.ts, src/app/api/ops/report/route.ts
  • src/lib/server/ops/ (config.ts, wiring.ts, instance.ts, handler.ts, monitor.ts, alerts.ts, state-store.ts, types.ts)
  • src/lib/server/ops/checks/ (rpc-health.ts, chain-events.ts, watched.ts, governance.ts, signer-config.ts, probe.ts, probe-fetch.ts, key-expiry.ts)
  • src/lib/server/ops/slo/ (history.ts, aggregate.ts, report.ts, report-handler.ts, report-instance.ts)
  • src/lib/server/pinning/check.ts
  • docs/process/runbooks/ (incident, keys, RPC and pinning, support)

Tests​

  • src/lib/server/ops/*.test.ts, src/lib/server/ops/checks/*.test.ts, src/lib/server/ops/slo/*.test.ts
  • src/lib/server/pinning/repair-and-check.test.ts
  • E2E: e2e/journeys/service-probe.journey.ts