Real time

The real-time stream pushes your workspace's alerts over a WebSocket connection as soon as they are created, with no public server to expose. It suits dashboards, internal bots and monitoring scripts.

Text
wss://aicscan.io/api/ws/alertes[?last_id=<id>]

Authentication#

Send your API key in the Authorization header of the opening request:

HTTP
GET /api/ws/alertes?last_id=1234 HTTP/1.1
Host: aicscan.io
Upgrade: websocket
Connection: Upgrade
Authorization: Bearer aic_k3J9xQ2a_...
  • The key needs the read scope; the API v1 rules apply: plan including the API, active workspace, per-key rate limit. Each opening counts as one request toward the rate limit and as one call toward the daily quota.
  • The connection only receives the alerts of the key's workspace.
  • The browser WebSocket API cannot send an Authorization header: this key-based stream is meant for your servers. (The Aicscan dashboard uses the same stream with the browser session.)

Refused at opening#

A refusal is an ordinary HTTP response, with the same error body as API v1:

HTTP Code
401 missing_api_key, invalid_api_key, api_key_revoked, api_key_expired
403 plan_required, workspace_suspended, account_under_review, insufficient_scope
429 rate_limited, daily_quota_exceeded
429 too_many_connections: more than 5 connections open with this key, or more than 20 for the workspace. Body { "error": "…", "code": "too_many_connections" }.

Access is checked again every 60 seconds: a revoked or expired key, a plan that no longer includes the API or a suspended workspace closes the connection with code 4401. Do not reconnect automatically after a 4401.

Server messages#

All messages are JSON objects sent as text, told apart by type.

hello#

First message of every connection:

JSON
{ "type": "hello", "workspace_id": "0193a1b2-0c1d-7e4f-8a9b-1c2d3e4f5a01", "last_id": 1234, "ping_secs": 25 }

last_id is the starting point of the stream: the requested value, or the latest existing alert if the connection did not ask for one (only the following alerts will be sent).

alert#

JSON
{
  "type": "alert",
  "id": 1235,
  "alert": {
    "id": 1235,
    "notification_id": "0193a1c9-41aa-7b2e-9c0d-1e2f3a4b5c6d",
    "type": "alert",
    "stage": "confirmed",
    "severity": "critical",
    "priority": 30,
    "rule_id": "0193a1b4-2e7f-7a10-9b3c-4d5e6f708192",
    "match_id": "0193a1c9-3f10-7d42-8a1b-2c3d4e5f6a7b",
    "title": "🔴 CRITICAL · Treasury: large USDT outflows · outgoing",
    "text": "🔴 CRITICAL · Treasury: large USDT outflows · outgoing\n✅ Confirmed · 15 confirmations\n…",
    "created_at": "2026-11-14T09:30:13.611Z"
  }
}
  • One alert and one stage (unconfirmed, confirmed, cancelled, escalation) produce a single message, even if the workspace has several channels. The text is the first channel's (its language).
  • id is strictly increasing for a workspace: it is the value to remember for resuming.
  • Alerts grouped in a digest and alerts held back by a channel's quiet hours are streamed here right away.
  • match_id lets you read the full alert: GET /v1/matches (event, raw amount, transaction hash).

gap#

JSON
{ "type": "gap", "from": 3234, "to": 9876 }

On resume, at most 2,000 missed alerts are replayed. If more are missing, the server sends the 2,000 oldest, then this message: alerts with ids from from + 1 to to are not sent, and the live stream resumes after to. Fill the gap with GET /v1/matches if needed.

pong#

JSON
{ "type": "pong", "last_id": 1235 }

Answer to an application-level ping from the client (see below).

Client messages#

Only { "type": "ping" } is interpreted; the server answers pong with the last id sent. Any other message is ignored. A message over 4 KiB closes the connection; more than 10 messages per second close it with code 1008.

Keeping the connection alive#

  • The server sends a WebSocket Ping frame every 25 seconds. Libraries answer with a Pong by themselves.
  • With no message at all from the client (a Pong is enough) for 60 seconds, the server closes the connection with code 1001.

Resuming after a disconnect#

  1. Remember the id of the last processed alert (persistently, if your program can restart).
  2. Reconnect with ?last_id=<id> (or the Last-Event-ID: <id> header): the server first sends the missed alerts, in order, then resumes the live stream.
  3. Alerts can be replayed for 7 days.
  4. Space out your attempts: 1 s, 2 s, 5 s, 10 s, then 30 s.
  5. An alert can arrive twice around a resume: skip any id lower than or equal to the last one processed.

Complete clients#

Both clients apply all the rules above: authentication, resume with last_id, duplicate removal, gap message, reconnection with increasing delays, stop on 4401 or on a refused key.

Node.js
// Aicscan real-time client (Node.js 18+, "ws" package: npm install ws).
import WebSocket from 'ws';

const URL = 'wss://aicscan.io/api/ws/alertes';
const DELAYS = [1, 2, 5, 10, 30]; // seconds between reconnections
let lastId = null; // persist it to resume after a restart
let failures = 0;
let stop = false; // key refused: no point retrying

function connect() {
  const url = lastId == null ? URL : `${URL}?last_id=${lastId}`;
  const ws = new WebSocket(url, {
    headers: { Authorization: `Bearer ${process.env.AICSCAN_API_KEY}` },
  });

  ws.on('message', (data) => {
    const msg = JSON.parse(data.toString());
    if (msg.type === 'hello') {
      failures = 0;
      lastId ??= msg.last_id;
    } else if (msg.type === 'alert') {
      if (lastId != null && msg.id <= lastId) return; // already received
      lastId = msg.id;
      const a = msg.alert;
      console.log(a.severity, a.stage, a.title);
    } else if (msg.type === 'gap') {
      console.warn(`alerts ${msg.from + 1} to ${msg.to} not replayed: see GET /v1/matches`);
    }
  });

  // Refused at opening: 401, 403, 429… with a JSON body.
  ws.on('unexpected-response', (_req, res) => {
    let body = '';
    res.on('data', (c) => (body += c));
    res.on('end', () => console.error('connection refused', res.statusCode, body));
    stop = res.statusCode === 401 || res.statusCode === 403;
  });

  ws.on('close', (code) => {
    if (stop) return;
    if (code === 4401) return console.error('access revoked or expired: not reconnecting');
    const delay = DELAYS[Math.min(failures++, DELAYS.length - 1)];
    setTimeout(connect, delay * 1000);
  });
  ws.on('error', () => {}); // a "close" event always follows
}

connect();

Both clients were run against a test server that reproduces the protocol (hello message, replay with a duplicate, gap, 1001 and 4401 closes, 401 refusal).

Limits#

Limit Value
Simultaneous connections per API key 5
Simultaneous connections per workspace 20
Incoming message 4 KiB at most, 10 per second at most
Server ping every 25 s
Close without answer after 60 s
Access check every 60 s
Replay on resume 2,000 alerts, 7 days

Close codes#

Code Meaning What to do
1001 Client silent for 60 s. Reconnect with last_id.
1008 Client sent more than 10 messages per second. Fix the client, then reconnect.
4401 Access revoked or expired (key, plan, workspace). Do not reconnect: check the key and the plan.
other, or no code Network failure, service restart. Reconnect with last_id, with increasing delays.