Monitoring & events
**OwlViz** change monitoring watches assets and collections for semantic metadata, health, authority, and image changes. Webhooks deliver monitor and market change events asynchronously (HMAC-signed POST, worker retries).
Base URL: https://api.owlviz.dev
Public reads vs writes
| Capability | Auth | Notes |
|---|---|---|
| List/read monitors, events, webhooks, deliveries | API key or admin | **Anonymous → 401** AUTOMATION_AUTH_REQUIRED. Keyed callers see **owned** rows only. Admin lists **ownerless ops** rows only |
| Read market watch by id | API key or admin | Same ownership rules as monitors |
| Collection market-watch status | public (ops watches only) | GET /v1/collections/:address/market-watch returns **ownerless** watches without auth; account-owned watches require key/admin (**404** when unauthenticated) |
| Create/update/disable monitors | API key or admin | Portal ov_live_… key (plan quota, min **3600s** interval) or ops admin token for ownerless operator monitors |
| Manual check | Bearer admin | POST /v1/monitors/:id/check — **not** available to API keys |
| Webhook mutations | API key or admin | Portal ov_live_… key on Builder+ (plan quota) or ops admin token |
**Self-serve change monitor writes** use your portal API key:
Authorization: Bearer ov_live_…Plan quotas (activeChangeMonitors): Free **1**, Builder **5**, Pro **20** (active, non-disabled monitors). Minimum intervalSeconds for keyed creates/updates is **3600**. Manual POST /v1/monitors/:id/check remains **admin-only** (MONITORING_ADMIN_TOKEN).
**Operator / legacy monitors** (owner_account_id null) are created with the admin token and may use tighter poll intervals configured server-side. Monitor-event webhook fanout for ownerless monitors delivers only to **ownerless** (ops) webhook endpoints. Account-owned monitors fan out only to webhook endpoints with the same owner_account_id. Market-event webhook fanout is unchanged (all matching subscriptions).
**Webhook and Market Watch writes** use your portal API key (ov_live_…) within plan quotas, or the same admin token for operator workflows. Manage subscriptions from the dashboard at /developer/webhooks, /developer/change-monitors, and /developer/market-watches without pasting keys into the browser.
Production default: PUBLIC_MONITORING_WRITES=false — **admin-path** monitor writes return **403** MONITORING_WRITES_FORBIDDEN when disabled (API key writes still work). Set MONITORING_ADMIN_TOKEN server-side only — never in NEXT_PUBLIC_* or client bundles.
The public scanner is **read-only** for change monitors — no create/check UI. Monitor, webhook, and Market Watch CRUD live in the developer portal. On asset/collection scanner pages, the Monitoring panel loads **your** change monitors via signed-in portal session (BFF); signed-out visitors see a sign-in CTA instead of anonymous monitor lists.
Monitor endpoints
| Method | Path | Auth |
|---|---|---|
POST | /v1/monitors | API key or admin write |
GET | /v1/monitors | API key or admin (owned / ops-only lists) |
GET | /v1/monitors/:id | API key or admin (404 cross-tenant) |
PATCH | /v1/monitors/:id | API key or admin write |
DELETE | /v1/monitors/:id | API key or admin write (soft disable) |
GET | /v1/monitors/:id/events | API key or admin (404 cross-tenant) |
POST | /v1/monitors/:id/check | admin write only |
Create monitor (self-serve)
curl -sS -X POST "$API_BASE/v1/monitors" \
-H "Authorization: Bearer ov_live_…" \
-H "Content-Type: application/json" \
-d '{
"targetType": "asset",
"target": "C5gHBKXwA8jduXNk3HyAVLnLBN6PEM8fTqkNNh5uyyjJ",
"intervalSeconds": 3600,
"events": ["metadata.changed", "health.degraded"]
}'Operator-only create (ownerless legacy monitor, tighter intervals allowed):
curl -sS -X POST "$API_BASE/v1/monitors" \
-H "Authorization: Bearer $MONITORING_ADMIN_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 accept a portal API key (Builder+ quota) or ops admin token. POST /v1/webhooks returns the signing secret **once** at creation — store it securely.
Subscription event types
Monitor events (asset/collection), market change events (spot_floor_changed, reference_floor_changed, provider_floor_changed, provider_status_changed), wildcard *, and synthetic webhook.test.
Delivery records (Sprint 6.2)
| Method | Path | Description | ||
|---|---|---|---|---|
GET | /v1/webhooks/:id | Endpoint plus deliveryHealth summary (last status, attempt, error, counts) | ||
GET | /v1/webhooks/:id/deliveries | History (?limit=, `?status=failed\ | dead\ | …`) |
GET | /v1/webhooks/:id/deliveries/:deliveryId | Single delivery inspection |
Each delivery includes monitorEventId or marketEventId, status, attempt, nextRetryAt, lastErrorCode, httpStatus, and responseTimeMs. Failed deliveries retry with exponential backoff until WEBHOOK_MAX_ATTEMPTS, then move to terminal dead. See [webhooks.md](../webhooks.md) for the full lifecycle.
Market change events (read API)
Durable market events are listed publicly — they are **not** mixed into GET /v1/events (monitor feed):
| Method | Path | Description |
|---|---|---|
GET | /v1/market-events | Global feed with filters |
GET | /v1/market-events/:id | Single event |
GET | /v1/collections/:address/market/events | Collection-scoped feed |
Event types: spot_floor_changed, reference_floor_changed, provider_floor_changed, provider_status_changed. See [Collections — Sprint 6](./collections.md#sprint-6--market-change-events-internal-durable-events) for detection semantics.
Subscribe to push delivery with POST /v1/webhooks and market event types in events. Example: [13-market-webhook.sh](./examples/13-market-webhook.sh).
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.