Stop polling. Two endpoints on the intelligence service push flag events as they are detected.

How live is “live”

There is no push path from the rug model into the streaming process. The model writes rows to a table; the stream polls that table every 60 seconds and publishes the difference.So a flag reaches you in up to one poll interval, not instantly. Every welcome event carries source.poll_interval_s so you can see the real number rather than assuming zero.
This is a deliberate, documented property, not a defect being hidden. When the model gains a push path the interval goes to zero and poll_interval_s will say so — your client needs no change.

Connect

Authentication

The token goes in an Authorization: Bearer header, or as ?token= for clients that cannot set headers — which includes every browser, because the WebSocket and EventSource constructors take a URL and nothing else.
Mint a separate token for streaming. A token in a URL is written to the platform’s access log, to any proxy in front of it, to the browser’s history, and to the Referer of anything the page loads afterwards. Its exposure surface is larger than a REST token’s, so it should be revocable on its own.The service redacts the value from its own logs. It cannot redact it from anybody else’s.
The header is checked first and always wins. Use it wherever you can.

Concurrent connection limits

Counted per credential, not per tier — the hundred-and-first Pro customer is never refused because of a stranger. Over the limit, the connection is refused with 429 before the WebSocket handshake completes, and the body says which limit you hit:
A scope of "instance" rather than a tier name means the server is full, not your plan. Retry; it is not a billing problem.

Filtering by chain

Set the initial filter in the query string, on either transport:
Omit it to receive every chain. On WebSocket you can also change it in place:
The server replies with a subscribed event carrying the resolved filter. Chain names not scored by this service are reported, not silently dropped:
Your existing filter is left alone when every requested name is rejected — a typo must not silently switch you to “everything” and a surprise bill.
SSE is one-way. Its filter is fixed for the life of the connection; reconnect with a different ?chains= to change it, or use /ws/flags.

Event types

Every event carries seq, type and ts. Chain-scoped events also carry chain.

rug_flag

first_seen: false means an existing flag was re-scored. A wallet moving from 62 to 91 is usually the event you most want, so re-scores are published rather than deduplicated away. top_evidence is the single top factor, not the full attribution. For the complete explanation read /api/token/{address}/rug-score, where it is authoritative.

Resuming without a gap

Every event carries a monotonic seq. Reconnect with the last one you saw:
EventSource does this for you — it sends Last-Event-ID, which the server treats identically. The replay window is bounded. If you ask for a range that has already been discarded, welcome carries a gap:
Those events are gone from the stream. Re-read the range from the REST API.

Backpressure

Each connection buffers 1,000 events. If you stop reading, the oldest are dropped — you get the current state of the world rather than a backlog you will never catch up on — and you are told exactly what you lost:
The lag event arrives before the event on the far side of the gap, so a jump in seq is never ambiguous. One slow subscriber costs the others nothing.

Heartbeats

Every 30 seconds of silence you get a heartbeat event, and on WebSocket a protocol-level ping as well. Both, because the JSON one is what your application can act on and the ping is what stops an intermediary reaping an idle connection. If you see neither for 90 seconds, the connection is dead — reconnect with ?since=.

Why the feed may be empty

An empty feed and a broken feed look identical unless you check. welcome tells you which one you have:
  • primed: false — the poller has not completed a first read. Nothing has been checked yet.
  • unavailable_reason non-null — the flag table could not be read. This is not a quiet day; nothing looked.
A subscriber who cannot tell “no wallets were flagged” from “wallets were flagged and you did not get them” is being sold the first while receiving the second. That is why the reason is on the wire.

Reaching /ws/flags over plain HTTP

You get a 426 with the URL you actually wanted, rather than a bare error:

Known limitations

  • Single instance. The event bus is in one process’s memory. The service runs one replica for this reason. Scaling out requires a shared log first — see ADR 0001 in the repository.
  • HEAD answers 501 on the deployed service — on every route, including /health. That handler defines no HEAD method and never has. Use GET.
  • The feed is only as populated as its source. Production currently serves no_snapshot on the flag table because its database credentials are unset, so the stream connects, authenticates, heartbeats, and reports unavailable_reason rather than delivering flags. Reported honestly rather than as a quiet day.
  • No permessage-deflate. Payloads are small JSON objects; the extension is opt-in and declining it is conformant.
Scienter publishes impersonal market signals of general and regular circulation. Nothing here is investment, legal, or tax advice, or a recommendation to buy, sell, or hold any asset. Scienter is not a registered investment adviser or broker-dealer. Trading digital assets can result in the total loss of your funds — see Disclaimers for the full text.