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.
wss://aicscan.io/api/ws/alertes[?last_id=<id>]Authentication#
Send your API key in the Authorization header of the opening request:
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
readscope; 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
WebSocketAPI cannot send anAuthorizationheader: 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:
{ "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#
{
"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). idis 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_idlets you read the full alert:GET /v1/matches(event, raw amount, transaction hash).
gap#
{ "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#
{ "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
Pingframe every 25 seconds. Libraries answer with aPongby themselves. - With no message at all from the client (a
Pongis enough) for 60 seconds, the server closes the connection with code1001.
Resuming after a disconnect#
- Remember the
idof the last processed alert (persistently, if your program can restart). - Reconnect with
?last_id=<id>(or theLast-Event-ID: <id>header): the server first sends the missed alerts, in order, then resumes the live stream. - Alerts can be replayed for 7 days.
- Space out your attempts: 1 s, 2 s, 5 s, 10 s, then 30 s.
- An alert can arrive twice around a resume: skip any
idlower 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.
// 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();# Aicscan real-time client (Python 3.9+, "websockets" package, version 14 or later).
import asyncio
import json
import os
import websockets
URL = "wss://aicscan.io/api/ws/alertes"
DELAYS = [1, 2, 5, 10, 30] # seconds between reconnections
async def main():
last_id = None # persist it to resume after a restart
failures = 0
headers = {"Authorization": f"Bearer {os.environ['AICSCAN_API_KEY']}"}
while True:
url = URL if last_id is None else f"{URL}?last_id={last_id}"
try:
async with websockets.connect(url, additional_headers=headers) as ws:
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "hello":
failures = 0
if last_id is None:
last_id = msg["last_id"]
elif msg["type"] == "alert":
if last_id is not None and msg["id"] <= last_id:
continue # already received
last_id = msg["id"]
alert = msg["alert"]
print(alert["severity"], alert["stage"], alert["title"])
elif msg["type"] == "gap":
print(f"alerts {msg['from'] + 1} to {msg['to']} not replayed: see GET /v1/matches")
except websockets.InvalidStatus as e: # refused at opening (401, 403, 429…)
print("connection refused", e.response.status_code, e.response.body.decode(errors="replace"))
if e.response.status_code in (401, 403):
return
except websockets.ConnectionClosed as e:
if e.rcvd is not None and e.rcvd.code == 4401:
print("access revoked or expired: not reconnecting")
return
except OSError:
pass # network unavailable
await asyncio.sleep(DELAYS[min(failures, len(DELAYS) - 1)])
failures += 1
asyncio.run(main())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. |