Temps réel
Le flux temps réel pousse les alertes de votre espace sur une connexion WebSocket, dès leur création, sans serveur public à exposer. Il convient aux tableaux de bord, aux robots internes et aux scripts de surveillance.
wss://aicscan.io/api/ws/alertes[?last_id=<id>]Authentification#
Envoyez votre clé d’API dans l’en-tête Authorization de la requête d’ouverture :
GET /api/ws/alertes?last_id=1234 HTTP/1.1
Host: aicscan.io
Upgrade: websocket
Connection: Upgrade
Authorization: Bearer aic_k3J9xQ2a_...- La clé doit avoir la portée
read; les règles de l’API v1 s’appliquent : offre comprenant l’API, espace actif, débit par clé. Chaque ouverture compte pour une requête du débit et pour un appel du quota quotidien. - La connexion ne reçoit que les alertes de l’espace de la clé.
- L’API
WebSocketdes navigateurs ne permet pas d’envoyer d’en-têteAuthorization: ce flux par clé est destiné à vos serveurs. (L’espace client d’Aicscan utilise le même flux avec la session du navigateur.)
Refus à l’ouverture#
Un refus est une réponse HTTP ordinaire, avec le même corps d’erreur que l’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 : plus de 5 connexions ouvertes avec cette clé, ou plus de 20 pour l’espace. Corps { "error": "…", "code": "too_many_connections" }. |
L’accès est revérifié toutes les 60 secondes : une clé révoquée ou expirée, une offre qui ne comprend plus l’API ou un espace suspendu ferment la connexion avec le code 4401. Ne vous reconnectez pas automatiquement après un 4401.
Messages du serveur#
Tous les messages sont des objets JSON en texte, distingués par type.
hello#
Premier message de chaque connexion :
{ "type": "hello", "workspace_id": "0193a1b2-0c1d-7e4f-8a9b-1c2d3e4f5a01", "last_id": 1234, "ping_secs": 25 }last_id est le point de départ du flux : la valeur demandée, ou la dernière alerte existante si la connexion n’en demandait pas (seules les alertes suivantes seront envoyées).
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"
}
}- Une alerte et une étape (
unconfirmed,confirmed,cancelled,escalation) donnent un seul message, même si l’espace a plusieurs canaux. Le texte est celui du premier canal servi (sa langue). idest strictement croissant pour un espace : c’est la valeur à mémoriser pour la reprise.- Les alertes regroupées dans un récapitulatif et celles retenues par les heures calmes d’un canal sont diffusées ici tout de suite.
match_idpermet de lire l’alerte complète :GET /v1/matches(événement, montant brut, hash de transaction).
gap#
{ "type": "gap", "from": 3234, "to": 9876 }À la reprise, au plus 2 000 alertes manquées sont rejouées. S’il en manque davantage, le serveur envoie les 2 000 plus anciennes, puis ce message : les alertes d’identifiant from + 1 à to ne sont pas envoyées, et le direct reprend après to. Comblez l’intervalle avec GET /v1/matches si besoin.
pong#
{ "type": "pong", "last_id": 1235 }Réponse à un ping applicatif du client (voir plus bas).
Messages du client#
Seul { "type": "ping" } est interprété ; le serveur répond pong avec le dernier identifiant envoyé. Tout autre message est ignoré. Un message de plus de 4 Kio ferme la connexion ; plus de 10 messages par seconde la ferment avec le code 1008.
Maintien de la connexion#
- Le serveur envoie une trame WebSocket
Pingtoutes les 25 secondes. Les bibliothèques y répondent seules par unPong. - Sans aucun message du client (un
Pongsuffit) pendant 60 secondes, le serveur ferme la connexion avec le code1001.
Reprise après coupure#
- Mémorisez l’
idde la dernière alerte traitée (durablement, si votre programme peut redémarrer). - Reconnectez-vous avec
?last_id=<id>(ou l’en-têteLast-Event-ID: <id>) : le serveur envoie d’abord les alertes manquées, dans l’ordre, puis reprend le direct. - Les alertes restent rejouables pendant 7 jours.
- Espacez les tentatives : 1 s, 2 s, 5 s, 10 s, puis 30 s.
- Une alerte peut arriver deux fois autour d’une reprise : ignorez tout
idinférieur ou égal au dernier traité.
Clients complets#
Les deux clients appliquent toutes les règles ci-dessus : authentification, reprise avec last_id, élimination des doublons, message gap, reconnexion avec délai croissant, arrêt sur 4401 ou sur une clé refusée.
// Client temps réel Aicscan (Node.js 18+, paquet « ws » : npm install ws).
import WebSocket from 'ws';
const URL = 'wss://aicscan.io/api/ws/alertes';
const DELAYS = [1, 2, 5, 10, 30]; // secondes entre deux reconnexions
let lastId = null; // à enregistrer durablement pour reprendre après un redémarrage
let failures = 0;
let stop = false; // clé refusée : inutile de réessayer
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; // déjà reçue
lastId = msg.id;
const a = msg.alert;
console.log(a.severity, a.stage, a.title);
} else if (msg.type === 'gap') {
console.warn(`alertes ${msg.from + 1} à ${msg.to} non rejouées : voir GET /v1/matches`);
}
});
// Refus à l'ouverture : 401, 403, 429… avec un corps JSON.
ws.on('unexpected-response', (_req, res) => {
let body = '';
res.on('data', (c) => (body += c));
res.on('end', () => console.error('connexion refusée', res.statusCode, body));
stop = res.statusCode === 401 || res.statusCode === 403;
});
ws.on('close', (code) => {
if (stop) return;
if (code === 4401) return console.error('accès révoqué ou expiré : reconnexion abandonnée');
const delay = DELAYS[Math.min(failures++, DELAYS.length - 1)];
setTimeout(connect, delay * 1000);
});
ws.on('error', () => {}); // l'événement « close » suit toujours
}
connect();# Client temps réel Aicscan (Python 3.9+, paquet « websockets » 14 ou plus récent).
import asyncio
import json
import os
import websockets
URL = "wss://aicscan.io/api/ws/alertes"
DELAYS = [1, 2, 5, 10, 30] # secondes entre deux reconnexions
async def main():
last_id = None # à enregistrer durablement pour reprendre après un redémarrage
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 # déjà reçue
last_id = msg["id"]
alert = msg["alert"]
print(alert["severity"], alert["stage"], alert["title"])
elif msg["type"] == "gap":
print(f"alertes {msg['from'] + 1} à {msg['to']} non rejouées : voir GET /v1/matches")
except websockets.InvalidStatus as e: # refus à l'ouverture (401, 403, 429…)
print("connexion refusée", 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("accès révoqué ou expiré : reconnexion abandonnée")
return
except OSError:
pass # réseau indisponible
await asyncio.sleep(DELAYS[min(failures, len(DELAYS) - 1)])
failures += 1
asyncio.run(main())Ces deux clients ont été exécutés contre un serveur de test qui reproduit le protocole (message hello, rejeu avec doublon, gap, fermetures 1001 et 4401, refus 401).
Limites#
| Limite | Valeur |
|---|---|
| Connexions simultanées par clé d’API | 5 |
| Connexions simultanées par espace | 20 |
| Message entrant | 4 Kio au plus, 10 par seconde au plus |
| Ping du serveur | toutes les 25 s |
| Fermeture sans réponse | après 60 s |
| Contrôle de l’accès | toutes les 60 s |
| Rejeu à la reprise | 2 000 alertes, 7 jours |
Codes de fermeture#
| Code | Sens | Que faire |
|---|---|---|
1001 |
Client sans réponse depuis 60 s. | Se reconnecter avec last_id. |
1008 |
Plus de 10 messages par seconde envoyés par le client. | Corriger le client, puis se reconnecter. |
4401 |
Accès révoqué ou expiré (clé, offre, espace). | Ne pas se reconnecter : vérifier la clé et l’offre. |
| autre, ou coupure sans code | Coupure réseau, redémarrage du service. | Se reconnecter avec last_id, avec un délai croissant. |