API Reference
WebSocket

WebSocket API

Superhero pushes live token data over Socket.IO. Two events carry token updates and trades on the default namespace, and a separate authenticated namespace carries notifications.

Connecting

NetworkURL
Mainnethttps://api.superhero.com
TestnetNot publicly documented

Same origin as the REST API, default namespace, default /socket.io/ path. No authentication and no handshake beyond Socket.IO's own.

import { io } from 'socket.io-client';
 
const socket = io('https://api.superhero.com', { autoConnect: false });
socket.connect();

The CORS origin is the same value the REST API uses: every origin by default, an allow-list when the operator sets ALLOWED_ORIGINS. See Conventions.

A plain WebSocket client cannot connect: Socket.IO is a protocol layered on Engine.IO, so use a Socket.IO client for your language.

Events

EventFires when
token-updatedA token's live price, supply, market cap or DAO balance has been re-read from chain and written back
token-historyA buy, sell or token creation has been indexed live

Both are broadcast to every connected client, so getting started takes two steps: connect and listen. Each payload carries the token's sale_address, so your process can route it to the views that show that token. For newly created tokens, read GET /api/tokens?order_by=created_at&order_direction=DESC or GET /api/feed.

All emissions originate server-side, from services that have already validated the payload.

Payloads

Both events are an envelope with the token's sale_address at the top level and the payload under data.

token-updated

{ "sale_address": "ct_...", "data": {} }

data is the live-price read that was just written to the token row, a fixed subset of TokenDto:

KeyTypeUnit
pricestringAE
price_dataPriceDtoAE
sell_pricestringAE
sell_price_dataPriceDtoAE
market_capstringaettos
market_cap_dataPriceDtoaettos
total_supplystringbase units
circulating_supplystringbase units
dao_balancestringaettos
addressstringthe AEX-9 contract
name, symbolstringfrom the sale contract's metaInfo
beneficiary_address, bonding_curve_address, owner_addressstringfrom metaInfo

Nothing else is on it. rank, trending_score, holders_count, tx_count, collection_info, performance and created_at are never pushed. Refetch /api/tokens/{address}/rankings and the holders endpoint for those, throttled to at most once every 15 seconds per sale_address so a burst of trades collapses into one refetch.

A field the chain read failed to return is absent rather than null. Merge, do not replace:

const newTokenData = { ...currentTokenData, ...message.data };

token-history

{ "sale_address": "ct_...", "data": {}, "token": {} }

token is the full token row the trade belongs to. data is the transaction record that was just persisted, with the fields of a GET /api/transactions row:

FieldTypeNotes
sale_addressstringRepeats the envelope's
tx_typestringbuy, sell or create_community
tx_hashstringth_...
block_heightnumber
addressstringThe trader's ak_...
volumestringwhole tokens
protocol_rewardstringwhole tokens
amountPriceDtoAE
unit_pricePriceDtoAE
previous_buy_pricePriceDtoAE
buy_pricePriceDtoAE
total_supplystringwhole tokens
market_capPriceDtoAE, not aettos
created_atstringISO 8601
verifiedboolean

The market_cap in data is in AE, while the market_cap on the same event's token object is in aettos.

For a candlestick chart, read data.buy_price[convertTo] and data.market_cap[convertTo] using the same nine currency codes as the REST API: update the current candle when the tick is inside the active interval and open a new one otherwise. For a live-trades list, spread data and token into one object and prepend it to your cached GET /api/transactions page.

Trades are pushed as they are indexed live. For history, read GET /api/transactions.

Delivery

Socket.IO reconnects on its own. Events are live signals: use each one as your cue to refetch, with the REST endpoints as the source of record. After a reconnect, re-read the REST endpoints behind the views you have open so they pick up anything that changed in between.

A minimal live-trades client

import { io } from 'socket.io-client';
 
const SALE_ADDRESS = 'ct_YOUR_SALE_ADDRESS';
const socket = io('https://api.superhero.com', { autoConnect: false });
 
socket.on('connect', () => console.log('connected'));
socket.on('disconnect', () => console.log('disconnected — refetch on reconnect'));
 
socket.on('token-history', (message) => {
  if (message.sale_address !== SALE_ADDRESS) return;
  // data.amount and data.unit_price are already in AE; volume is in whole tokens
  console.log('trade', message.data.tx_type, message.data.volume, 'for', message.data.amount.ae, 'AE');
});
 
socket.on('token-updated', (message) => {
  if (message.sale_address !== SALE_ADDRESS) return;
  console.log('price now', message.data.price_data?.ae, 'AE');
});
 
socket.connect();

Seed from REST first, then let the socket keep it current:

curl -s "https://api.superhero.com/api/transactions?token_address=ct_YOUR_SALE_ADDRESS&includes=token&limit=50"

The notification namespace

Notifications run on the same origin but a different Socket.IO namespace, with a different connection model:

namespace:  https://api.superhero.com/notifications
auth:       { token }   — a bearer session token
transports: ['polling'] with upgrade: false, forceNew: true
Token socketNotification socket
NamespaceDefault (/)/notifications
AuthenticationNoneSocket.IO auth.token, from the feed session flow
TransportPolling, upgrading to WebSocketPolling only, upgrade disabled
ReconnectionLibrary defaultreconnectionAttempts: 5
Eventstoken-updated, token-historynotification, unread-count
Fan-outBroadcast to everyoneOnly to the proven address's own room

The token comes from the feed session flow in Conventions: challenge, wallet signature, then a bearer token.

Handshake

The server runs four checks in order and disconnects the socket on any failure:

  1. Per-IP handshake cap. At most 30 handshake attempts per source IP per rolling minute, counted before any session lookup.
  2. Token extraction. handshake.auth.token first, then an Authorization: Bearer header. No token means immediate disconnect.
  3. Session resolution. The token is an opaque 32-byte random hex string mapped to its owner address, minted by POST /api/notifications/{address}/feed/session and valid for 7 days. It is not a JWT and is revocable server-side. An unknown or expired token means disconnect.
  4. Per-address connection cap. At most 10 concurrent sockets per address.

On success the socket joins a room named after the proven address, and every emit is addressed to that room. The namespace accepts no client-sent messages: read history and mark items read over the authenticated REST endpoints.

notification carries a feed item; unread-count carries { count }.

The persisted feed is pruned after 90 days and trimmed to the 500 most recent rows per address; a feed page is capped at 50 items. The REST side of the same subsystem:

POST /api/notifications/{address}/feed/challenge
POST /api/notifications/{address}/feed/session
GET  /api/notifications/{address}/feed?limit=50
GET  /api/notifications/{address}/feed/unread-count
POST /api/notifications/{address}/feed/read
POST /api/notifications/{address}/web-push/subscription
GET  /api/notifications/web-push/vapid-public-key

The feed page returns { items, nextCursor }; the unread count returns { count }; marking read takes an optional array of ids (omit it to mark everything) and returns the new count.

What is not on the WebSocket

New posts, comments and tips are not pushed on the token channels, so poll the feed over REST. Chat runs on Nostr relays (wss://relay.superhero.chat by default), not on this socket. Chain events are also absent; for those, watch the middleware.

Related