Skip to main content
Instead of polling /audit-contract to keep a token’s verdict fresh, subscribe it once and we push every change to you. Whenever a verdict actually moves (a safe↔unsafe flip, an owner change, a gate or honeypot flipping, or a closed-source token re-audited on its now-verified source), we deliver it over a live SSE stream or a webhook. Polling still works exactly as before; push delivery is purely additive. A subscription costs a one-time 2 credits per token (only genuinely-new tokens are charged), and any re-audit the watcher triggers is free (included). See Credits & Billing. Two ways to receive the pushes (use either or both):
  • Live stream (SSE). Hold an outbound GET /api/stream open and we push changes down it. No inbound port to expose: it’s a normal long-lived HTTPS request with your API key. See Live stream below.
  • Webhook. We POST each change to an HTTPS URL you host.
Same events, same per-token subscriptions, same billing. The transport is your choice.
Subscribing and delivery are independent. You can subscribe tokens without a stream or webhook configured. You’re still charged, and you pull the changes yourself from GET /api/events. A webhook or an open stream only controls whether changes are pushed to you; neither is a prerequisite for subscribing.

Live stream (SSE)

You open an outbound connection to us and we push events over it. Nothing to expose, no inbound port. It’s a long-lived HTTPS GET, so a locked-down environment accepts it like any other API call. Just connect. Hold the request open and events arrive as they happen. Nothing to enable; it’s a plain authenticated endpoint, and you only receive changes for tokens you’re subscribed to:
A silent stream almost always means you have no subscriptions yet. The connection opens and authenticates even with zero subscribed tokens, and then stays quiet forever, because there is nothing to push. If all you see is the opening ready event followed by : keepalives, check GET /api/subscriptions before debugging anything else. Subscribe as you go with subscribe=true on your audit calls, or in bulk with import-history.
You get Server-Sent Events: each change is one event: audit.changed frame whose data: is the same payload we deliver to a webhook (see The event you receive) and whose SSE id: is that event’s sequential paging id (what you resume from, and not the event_id UUID inside the payload).
  • Resume with no gaps. On reconnect, send the last id back. The browser EventSource does it automatically via Last-Event-ID; from code, pass the header or ?since=<id>. We replay every event after it, so a dropped connection never loses a change. A fresh connect (no id) streams new events only; ?since=0 replays your whole history.
  • It’s scoped to your active subscriptions, exactly like a webhook and /events.
  • Keepalive. We send a : comment every ~25s so proxies keep the connection open. Reconnect on any disconnect (add your own backoff).
  • Both at once is fine. If you also have a webhook set, you’ll receive each change on both, so dedupe on event_id.
Browser example:

Set up a webhook

Register your HTTPS URL and get a signing secret (returned once, so store it):
Updating the URL keeps the secret; pass {"rotate": true} to force a new one. POST /api/webhook/test sends a signed ping so you can confirm wiring before subscribing anything. GET /api/webhook returns the current config (never the secret) plus 24h delivery health:
The ping sent by /api/webhook/test has type: "ping" (not "audit.changed") and a non-UUID event_id / x-serialized-event: ping. Ignore it or 200 it; don’t parse it as a real event.

Subscribe tokens

A subscribe returns { "ok": true, "subscribed": <total>, "charged": <newly-created> }. Only charged tokens (genuinely new) cost credits. To backfill, POST /api/subscriptions/import-history subscribes every token you’ve called in the last 15 days that is already at the current audit version. It’s a two-step flow so you never get a surprise bill: a bare call is a dry-run quote (creates/charges nothing):
Re-POST with ?confirm=true to actually subscribe and be charged (returns the same { ok, subscribed, charged } shape as above).
A bulk subscribe (POST /api/subscriptions or import-history?confirm=true) is cap-gated: if the projected cost would cross your allowance it is denied with 402/429 (action: "raise_cap", plus required_credits / remaining_credits) before anything is subscribed. Nothing partial happens. Run the dry-run first to size the spend.
Fire an audit and get the result pushed, no polling: call /audit-contract with async=true&subscribe=true. It starts the audit, subscribes the token, and pushes the finished verdict to you (stream or webhook) when it lands. See the subscribe parameter.

The event you receive

Every change (on the stream or your webhook) carries the same JSON (type: "audit.changed"):
  • audit is the complete new verdict: the exact same object you get back from /audit-contract for this token. Do store[address] = payload.audit and you’re done; no re-fetch. (It’s null only if the change left no audit, e.g. a manual lock removed.)
  • change is a compact summary for cheap routing/alerting (badge flips, “a critical was added”) without parsing the full verdict.
  • diff tells you which findings moved: added / removed are full vuln objects, changed is { before, after, fields }.
event_id and the paging id are two different values. event_id is a UUID that identifies the event itself and never changes, even if it is delivered twice — dedupe on it. The paging id is a separate sequential number: it is the SSE frame’s id:, the id field on each /api/events element, and what you pass back as sincepage on it.
On a webhook, this JSON is the POST body, with headers x-serialized-signature: t=<unix>,v1=<hmac> and x-serialized-event: <event_id>. On the stream, it’s the SSE frame’s data:, and the frame’s id: is the sequential paging id (what Last-Event-ID resumes from).

Verify the webhook signature

Webhook POSTs are HMAC-signed (the stream needs no verification; it’s authenticated by your API key on the connection). v1 = HMAC_SHA256(secret, "<t>.<raw_body>"). Verify against the raw body bytes, in constant time, and reject if t is more than 300s off from now:

Delivery guarantees

  • At-least-once. We retry once (~30s after the first failure) on any non-2xx/timeout, then dead-letter. A push can arrive more than once, so dedupe on event_id.
  • Respond 2xx fast (10s timeout, webhook). Verify → enqueue → 200; do your work async.
  • Nothing is lost. Every change is written to a durable outbox. Replay anything you missed: GET /api/events?since=<last_id>&limit=1000{ "events": [...], "cursor": <id> }. Page with since=cursor. This is your catch-up path after downtime (and what the stream’s Last-Event-ID resume draws on). /events is scoped to your currently-active subscriptions: unsubscribing a token (DELETE) also hides its past events from replay, so drain what you need before unsubscribing. Each element is the same audit.changed object shown above, plus an id — the sequential paging id. cursor is the id of the last element (a number, echoing since on an empty page).
    since means the opposite here than on the stream. On /api/events, omitting since is the same as since=0 and replays your whole history from the beginning. On /api/stream, omitting it gives you new events only. In practice, always call /api/events with since=<the last id you processed>.
  • We pace webhook delivery (~3 pushes/sec to one endpoint), so a big initial catch-up is spread out, never dumped at once. Steady-state is well under 1/sec.

Endpoints

All authenticate with your API key in X-Auth-Key.