Monitoring & events
Change monitoring watches assets and collections for semantic metadata, health, authority, and image changes. Webhooks deliver events asynchronously.
Base URL: https://solana-metadata-platform-production.up.railway.app
Public reads vs private-beta writes
| Capability | Auth | Notes |
|---|---|---|
| List/read monitors | none | GET /v1/monitors, GET /v1/monitors/:id |
| List/read events | none | GET /v1/events, GET /v1/monitors/:id/events |
| List/read webhooks | none | Secrets are never returned after create |
| Create/update monitors | Bearer | Private beta only |
| Manual check | Bearer | POST /v1/monitors/:id/check |
| Webhook mutations | Bearer | Create, update, delete, test |
Write APIs require:
Authorization: Bearer <private-beta-token>Production default: PUBLIC_MONITORING_WRITES=false — writes return **403** MONITORING_WRITES_FORBIDDEN. When enabled, MONITORING_ADMIN_TOKEN must be configured server-side.
The public scanner dashboard is **read-only** for monitoring — no create/check UI, no embedded admin token.
Monitor endpoints
| Method | Path | Auth |
|---|---|---|
POST | /v1/monitors | Bearer write |
GET | /v1/monitors | public |
GET | /v1/monitors/:id | public |
PATCH | /v1/monitors/:id | Bearer write |
DELETE | /v1/monitors/:id | Bearer write (soft disable) |
GET | /v1/monitors/:id/events | public |
POST | /v1/monitors/:id/check | Bearer write |
Create monitor (private beta)
curl -sS -X POST "$API_BASE/v1/monitors" \
-H "Authorization: Bearer <private-beta-token>" \
-H "Content-Type: application/json" \
-d '{
"targetType": "asset",
"target": "C5gHBKXwA8jduXNk3HyAVLnLBN6PEM8fTqkNNh5uyyjJ",
"intervalSeconds": 3600,
"events": ["metadata.changed", "health.degraded"]
}'Monitor fields: targetType (asset | collection), target (mint/address), intervalSeconds, events (non-empty array from allowed enum).
Serialized monitor includes waitingForIndex (true when a collection monitor awaits indexing), lastCheckedAt, nextCheckAt, consecutiveFailures, etc.
Event types (actual enum members)
Asset events
| Type | Meaning |
|---|---|
metadata.changed | Semantic metadata change |
metadata.uri.changed | On-chain metadata URI changed |
metadata.failed | Off-chain metadata resolution failed |
metadata.recovered | Off-chain metadata recovered |
image.failed | Image reliability failure |
image.recovered | Image reliability recovered |
image.changed | Image URI/content change |
health.degraded | Health band worsened |
health.improved | Health band improved |
authority.changed | Any authority field changed |
mint_authority.changed | Mint authority changed |
freeze_authority.changed | Freeze authority changed |
update_authority.changed | Update authority changed |
permanent_delegate.changed | Permanent delegate changed |
token_program.changed | Token program changed |
collection.changed | Embedded collection ref changed |
owner.changed | Owner changed |
Collection events
| Type | Meaning |
|---|---|
collection.health.degraded | Aggregate health band worsened |
collection.health.improved | Aggregate health band improved |
collection.asset_count.changed | Indexed/discovered count changed |
collection.verification.changed | Verification counts changed |
collection.standard_distribution.changed | Metadata standard buckets changed |
collection.token_program_distribution.changed | Token program buckets changed |
Webhook test
| Type | Meaning |
|---|---|
webhook.test | Test delivery only |
Event listing
GET /v1/events — global feed with filters:
| Query | Description |
|---|---|
target | Mint or collection address |
targetType | asset or collection |
eventType | Event type string |
severity | info, warning, error, critical |
since | ISO timestamp |
limit | Page size |
cursor | Pagination cursor |
Event payload:
{
"data": [
{
"id": "...",
"monitorId": "...",
"targetType": "asset",
"target": "...",
"type": "health.degraded",
"severity": "warning",
"summary": "...",
"before": {},
"after": {},
"diff": { "changes": [] },
"snapshotIdBefore": "...",
"snapshotIdAfter": "...",
"occurredAt": "...",
"createdAt": "..."
}
],
"pagination": { "nextCursor": null }
}Semantic diff behavior
- **Baseline on first success** — the first successful check stores a baseline; later checks diff against it.
- **Excluded operational fields** — cache timestamps, transient resolver diagnostics, and similar fields are excluded from semantic diffs.
- **Attribute/creator normalization** — attributes and creators are normalized before comparison to reduce false positives.
- **Health band hysteresis** — health events fire on band changes (
healthy/warning/degraded/critical), not every point change. - **Image/metadata hysteresis** — image and metadata failure/recovery events use hysteresis to avoid flapping.
- **Event fingerprints** — each persisted event gets a deterministic SHA-256 fingerprint from
monitorId, event type, and semantic before/after state hashes. The database enforces a unique index onfingerprintwithON CONFLICT DO NOTHING, so repeated checks that detect the same semantic state do not insert duplicate events or re-trigger webhook delivery storms. Content hashes also participate in image change detection when available.
Image monitoring limitation
Asset image checks read **stored media-recovery state** from the database rather than actively revalidating media on every monitor cycle. Events may lag until media recovery runs elsewhere.
Webhooks
See [webhooks.md](../webhooks.md) for delivery headers, HMAC verification, retries, and payload shape.
Webhook write routes use the same Bearer gate as monitor writes. POST /v1/webhooks returns the signing secret **once** at creation — store it securely.
Collection monitor freshness
COLLECTION_MONITOR_FRESHNESS_SECONDS (default **3600**): collection monitors treat indexes older than this threshold as stale and re-enqueue background indexing. Monitors may report waitingForIndex: true until coverage is sufficient.