WebSocket API
WS /live: one socket, many subscriptions (trades, candles, token, pool, holders, balances, positions, pulse), its messages, metering, caps and errors.
Trades, candles, token and pool pages, holders, wallets and Pulse lists, pushed over one WebSocket as blocks are indexed. The socket covers the protocol, metering and caps; each chapter after it is one channel.
Live data comes over one WebSocket, /live. Open it once, then subscribe to as many things as you follow: each subscription has an id you choose, and every message for it carries that id. What a subscription follows is fixed by its subscribe message; to follow something else, unsubscribe, or subscribe again with the same id (it replaces the old one).
URL:
wss://app.aurelia.build/api/live[?apiKey=<key> | ?jwt=<jwt>]
ws://localhost:7070/live the API directly, locally
wss://<api-domain>/live in production
Messages
| Direction | Message | Meaning |
|---|---|---|
| You send | {"op":"subscribe","id","channel","chain",…parameters} | Start a subscription. id is 1–64 characters; parameters are strings or numbers. |
| You send | {"op":"unsubscribe","id"} | End it; answered with {"type":"unsubscribed","id"}. |
| You receive | {"type":"subscribed","id","channel","chain",…} | The subscription holds; its parameters, normalized. A snapshot channel's first message follows at once. |
| You receive | {"id","type":"<kind>","data"} | The subscription's messages: trades, candle, token, pool, tick, holders, balances, positions, position, pulse. |
| You receive | {"id","type":"error","status","error"} | Ends that subscription only; the socket stays open. id is null for a message that named none. |
| You receive | {"type":"resubscribed","chain"} | The server's feed came back after an outage: what was indexed meanwhile was not sent. Snapshot channels send their snapshot again; for trades and candles, read the recent ones over REST. |
Channels
| Channel | Parameters besides chain | Messages | Read |
|---|---|---|---|
trades | Exactly one of token, pool, wallet | trades: the scope's rows of each indexed batch, newest first, as GET /trades | Trades |
candles | pool or token; res (1m), currency (usd) | candle: the bar in progress, then each bar's final values | Candles |
token | token | token: the token body; tick: the price between bodies | Token and pool pages |
pool | pool | pool: the pool body; tick | Token and pool pages |
holders | token | holders: the holders body, its 100 largest | Holders |
balances | wallet | balances: the portfolio body | Balances and positions |
positions | wallet, optionally token | positions (200 most recent), or position (one token) | Balances and positions |
pulse | view and the filters of Pulse | pulse: the list's order, and the rows that changed | Pulse lists |
Metering and caps
- Credentials go in the URL (
apiKey=orjwt=), or the console's session cookie: browsers cannot set headers on a WebSocket. Opening the socket costs one credit. - Each subscribe to a known channel with a chain and known parameters costs one credit, and is refused like a request:
429 rate_limited(withretryAfter: subscribe again after it),402 insufficient_balance,401 unauthorized(then the socket closes with4401). Unsubscribing is free. - Each data message a subscription receives costs 0.3 credits, charged every second;
subscribed,unsubscribed,resubscribed,retractand errors are free. When your credits run out your sockets close with4402: add credit, then connect again. Your account page shows messages per channel and per API key, with the data sent and the time your sockets were open. - A socket holds at most its plan's subscriptions, and of them its plan's
balancessubscriptions: past them a subscribe is refused with429 too_many_subscriptionsor429 too_many_balances. Open sockets per account are capped too. The numbers are inGET /plans,live: a free account has 5 sockets with 25 subscriptions (5balances) each. - What a channel computes is computed once per subject for every socket that follows it, so subscribing to a busy token costs the server no more than a quiet one.
Errors and closing
- An error message ends one subscription. Its
statusis the one its REST route would answer:400for a bad channel or parameter,404for an unknown token or pool,422or502as the route has them,503when it cannot be served now (retry with backoff). A 4xx means fix the subscription before asking again. - The socket itself closes only for access:
4401(a bad or revoked key or JWT: stop),4402(out of credit: add credit, then reconnect),4429(burst, or too many open sockets: back off;retryAftersays how long when known). The reason is JSON, such as{"error":"unauthorized"}. A client too slow to take its messages is closed rather than skipped: reconnect and subscribe again. - Nothing is replayed after a reconnect: subscribe again, and read over REST what the page shows.
Captured on a test instance: an unknown channel, and an unsubscribe:
{"op":"subscribe","id":"z","channel":"prices","chain":"evm:56"}
{"id":"z","type":"error","status":400,"error":"channel must be one of trades, candles, token, pool, holders, balances, positions, pulse"}
{"op":"unsubscribe","id":"t1"}
{"type":"unsubscribed","id":"t1"}
A client
One socket per page or process: subscribe on open, route each message by its id, take a REST snapshot on subscribed and resubscribed, and on a drop reconnect with backoff and subscribe everything again.
Live clients, Browser:
// Live trades of one token in a web page: one socket, a trades subscription, a REST snapshot
// on every (re)subscription, and reconnection. Same origin as the Aurelia console, whose /api
// forwards to the API: your signed-in session is the credential.
const API = "/api";
const CHAIN = "solana";
const TOKEN = "So11111111111111111111111111111111111111112"; // WSOL
const list = document.body.appendChild(document.createElement("ol"));
let trades = []; // newest first
const key = (t) => `${t.tx}:${t.pool}:${t.eventIndex}`;
const newest = (a, b) =>
b.time - a.time || b.block - a.block || b.txIndex - a.txIndex || b.eventIndex - a.eventIndex;
function merge(batch) {
const byKey = new Map(trades.map((t) => [key(t), t]));
for (const t of batch) byKey.set(key(t), t);
trades = [...byKey.values()].sort(newest).slice(0, 200);
list.replaceChildren(...trades.slice(0, 50).map((t) => {
const li = document.createElement("li");
const usd = t.priceUsd === null ? "unpriced" : `$${t.priceUsd.toPrecision(4)}`;
li.textContent = `${new Date(t.time).toLocaleTimeString()} ${t.side} ${t.amount} at ${usd} ${t.labels.join(" ")}`;
return li;
}));
}
// Swaps indexed while this subscription was not live were not sent: fetch them.
async function snapshot() {
const res = await fetch(`${API}/trades?chain=${CHAIN}&token=${TOKEN}&limit=50`);
if (res.ok) merge((await res.json()).trades);
}
let delay = 1000;
function connect() {
const url = new URL(`${API}/live`, location.href);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(url);
ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", id: "sol", channel: "trades", chain: CHAIN, token: TOKEN }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "subscribed") delay = 1000;
if (msg.type === "subscribed" || msg.type === "resubscribed") snapshot();
else if (msg.type === "trades") merge(msg.data);
else if (msg.type === "error") console.error(msg.status, msg.error); // that subscription ended
};
ws.onclose = (e) => {
if (e.code === 4401 || e.code === 4402) return console.error("refused:", e.reason); // fix the key or add credit: do not retry
let wait = delay;
if (e.code === 4429) {
try { wait = Math.max(wait, JSON.parse(e.reason).retryAfter * 1000 || 60_000); } catch {}
}
setTimeout(connect, wait);
delay = Math.min(delay * 2, 30_000);
};
}
connect();
Live clients, Bun / Node:
// live.mjs: `bun live.mjs` or `node live.mjs` (Node 22+ has fetch and WebSocket built in).
// Prints a pool's swaps as they are indexed, and reconnects when the connection drops.
const API = process.env.AURELIA_API ?? "https://app.aurelia.build/api";
const KEY = process.env.AURELIA_KEY; // your API key, from your account page
const CHAIN = "solana";
const POOL = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE"; // SOL / USDC, Orca
const seen = new Set();
function print(batch) {
for (const t of [...batch].reverse()) { // batches are newest first; print oldest first
const key = `${t.tx}:${t.pool}:${t.eventIndex}`;
if (seen.has(key)) continue;
if (seen.size > 10_000) seen.clear();
seen.add(key);
const usd = t.priced ? `$${t.priceUsd.toFixed(4)}` : "unpriced";
console.log(new Date(t.time).toISOString(), t.side, t.amount, t.token.symbol, usd, t.labels.join(" "));
}
}
// Swaps indexed while this subscription was not live were not sent: fetch them.
async function snapshot() {
const res = await fetch(`${API}/trades?chain=${CHAIN}&pool=${POOL}&limit=20`, {
headers: { authorization: `Bearer ${KEY}` },
});
if (res.ok) print((await res.json()).trades);
else console.error("snapshot failed:", res.status, await res.text());
}
let delay = 1000;
function connect() {
const ws = new WebSocket(`${API.replace(/^http/, "ws")}/live?apiKey=${KEY}`);
ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", id: "pool", channel: "trades", chain: CHAIN, pool: POOL }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "subscribed") delay = 1000;
if (msg.type === "subscribed" || msg.type === "resubscribed") snapshot();
else if (msg.type === "trades") print(msg.data);
else if (msg.type === "error") console.error("subscription ended:", msg.status, msg.error);
};
ws.onclose = (e) => {
if (e.code === 4401 || e.code === 4402) {
console.error("refused:", e.reason); // bad credentials or no credit: do not retry
process.exit(1);
}
let wait = delay;
if (e.code === 4429) {
try { wait = Math.max(wait, JSON.parse(e.reason).retryAfter * 1000 || 60_000); } catch {}
}
console.error(`closed (${e.code}), reconnecting in ${wait} ms`);
setTimeout(connect, wait);
delay = Math.min(delay * 2, 30_000);
};
}
connect();
Live clients, Python:
# live.py: pip install requests websockets, then python live.py
# Prints a pool's swaps as they are indexed, and reconnects when the connection drops.
import asyncio, json, os, sys
from datetime import datetime, timezone
import requests
import websockets
API = os.environ.get("AURELIA_API", "https://app.aurelia.build/api")
KEY = os.environ["AURELIA_KEY"] # your API key, from your account page
CHAIN = "solana"
POOL = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE" # SOL / USDC, Orca
seen = set()
def show(batch):
for t in reversed(batch): # batches are newest first; print oldest first
key = (t["tx"], t["pool"], t["eventIndex"])
if key in seen:
continue
seen.add(key)
when = datetime.fromtimestamp(t["time"] / 1000, timezone.utc).isoformat()
usd = f"${t['priceUsd']:.4f}" if t["priced"] else "unpriced"
print(when, t["side"], t["amount"], t["token"]["symbol"], usd, " ".join(t["labels"]), flush=True)
def snapshot():
# Swaps indexed while this subscription was not live were not sent: fetch them.
headers = {"Authorization": f"Bearer {KEY}"}
r = requests.get(f"{API}/trades", params={"chain": CHAIN, "pool": POOL, "limit": 20}, headers=headers, timeout=10)
r.raise_for_status()
show(r.json()["trades"])
async def main():
url = API.replace("http", "ws", 1) + f"/live?apiKey={KEY}"
subscribe = {"op": "subscribe", "id": "pool", "channel": "trades", "chain": CHAIN, "pool": POOL}
delay = 1
while True:
wait = delay
try:
async with websockets.connect(url) as ws:
await ws.send(json.dumps(subscribe))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "subscribed":
delay = 1
if msg["type"] in ("subscribed", "resubscribed"):
await asyncio.to_thread(snapshot)
elif msg["type"] == "trades":
show(msg["data"])
elif msg["type"] == "error":
print("subscription ended:", msg["status"], msg["error"], file=sys.stderr)
except (OSError, websockets.WebSocketException) as e:
close = getattr(e, "rcvd", None)
if close and close.code in (4401, 4402):
sys.exit(f"refused: {close.reason}") # bad credentials or no credit: do not retry
if close and close.code == 4429:
try:
wait = max(wait, json.loads(close.reason).get("retryAfter") or 60)
except ValueError:
wait = max(wait, 60)
print(f"disconnected ({e}), reconnecting in {wait} s", file=sys.stderr)
await asyncio.sleep(wait)
delay = min(delay * 2, 30)
asyncio.run(main())