curl --request GET \
--url https://api.fomoscan.sh/v2/ws \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.fomoscan.sh/v2/ws', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.fomoscan.sh/v2/ws"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)Live theses, every token or a token list (WebSocket)
The live thesis feed. pump.fun callouts have their own socket at
/v2/ws/pump, described below. Attach
once, then declare what you want, every token or a list of tokens, and
change it at any time without reconnecting.
This path is an upgrade, not a request. A plain GET returns 404;
only a request carrying the WebSocket handshake headers is served.
The key cannot come from a browser. Authentication is the same
authorization: Bearer <key> header as every other endpoint, and the
browser WebSocket constructor cannot set headers. Hold the socket in your
backend and relay to your front end from there.
The socket is included from Growth ($899). Growth, Scale and Enterprise
can connect. Keys on legacy plans keep the websocket access they already
have. Self-serve free keys, Starter and Builder are refused during the
handshake with 403 WEBSOCKET_PLAN_REQUIRED.
A key that lacks the scope for this path is refused during the handshake
with a 403, before the upgrade, so you read it as an HTTP status rather
than a dropped socket.
Subscribing
A connection holds one subscription. It is one of two shapes:
{ "type": "subscribe", "subscription": { "type": "all" } }
{ "type": "subscribe", "subscription": { "type": "tokens", "tokens": ["So1111…", "0xabc…"] } }
A subscribe replaces whatever you had before — it does not add to it.
To change your watchlist, send the new list in full. To stop receiving:
{ "type": "unsubscribe" }
That is the one thing here likely to catch you out, so we say it out loud
on the wire too — see the replaced frame below.
You attach unsubscribed and receive nothing until you ask. A tokens
subscription may name up to 50 addresses. Past that, use all: it is
one subscription and it already carries every token.
Frames you receive
On attach:
{ "type": "ready", "connectionId": "4f9c…" }
After every accepted frame, your current subscription — the whole of it, so
there is nothing to reconcile (null once you unsubscribe):
{ "type": "subscription", "subscription": { "type": "tokens", "tokens": ["So1111…"] } }
And, when a subscribe tore down an earlier subscription, one frame ahead
of it saying what stopped:
{ "type": "replaced", "previous": { "type": "tokens", "tokens": ["EPjF…"] } }
The two always arrive in that order, so a client applying frames as they
land never ends up holding the dead subscription. A rejected subscribe
produces neither — nothing was torn down.
Then one frame per matching thesis:
{ "type": "thesis", "data": { "id": "…", "tokenAddress": "…", "tokenNetwork": "sol", … } }
data is the same object GET /v2/thesis/token/{tokenAddress} returns
in items — same field names (including authorAvatarUrl and
tokenImageUrl), same id, same epoch-millisecond timestamps. It is not a
second shape to code against. Read data.tokenAddress to route a frame back
to its token.
authorAvatarUrl points at our image host, at a path of the form
/a/<key>.webp. A new picture gets a new URL. A picture added in the last
few minutes can show a neutral placeholder until it is ready.
The pump feed
pump.fun callouts are on wss://api.fomoscan.sh/v2/ws/pump. Same
handshake, same plan rule, same subscribe messages and the same
per-minute price as this path. The path picks the feed, so the messages
are identical:
{ "type": "subscribe", "subscription": { "type": "all" } }
{ "type": "subscribe", "subscription": { "type": "tokens", "tokens": ["So1111…"] } }
To receive theses and callouts at once, open one socket on each path. Each socket is metered on its own.
A subscription on /v2/ws/pump receives one frame per new callout:
{ "type": "pump_callout", "callout": { "id": "…", "wallet": "…", "coinMint": "…", "thesis": "…", "createdAt": … } }
callout is the same object GET /v2/pump/thesis returns in items —
same field names, same id, same epoch-millisecond createdAt. Read
callout.coinMint to route a frame back to its token.
There is no replay on this feed either. Take a snapshot from
GET /v2/pump/thesis and reconcile on callout.id, the same id that
endpoint returns. A callout is sent once per socket, and only while it is
recent. Anything older is on GET /v2/pump/thesis.
A rejected frame is answered, does not close the socket, and does not change what you are subscribed to — so a typo costs you nothing:
{ "type": "error", "error": "too many addresses: 91 named, 50 is the cap; subscribe to \"all\" to receive every token" }
What you have to know before you build on it
- There is no replay. You hear what arrives after you subscribe,
nothing before it. Take a snapshot from the REST endpoint
(
GET /v2/thesis/token/{tokenAddress}, orGET /v2/pump/thesison the pump socket) and reconcile onid, which is stable and is the same id the REST endpoint returns. If the socket drops, fetch the newest page from the REST endpoint to close the gap, then reattach and resubscribe. - Subscriptions do not survive a reconnect. A new socket starts empty.
- A thesis is sent once. It is never re-sent on the same socket.
- The socket carries recent theses. Anything outside that window is on the REST endpoint instead.
- Frames are not ordered by
fomoCreatedAt. Sort client-side if order matters. nullmeans the value was not observed. An absentlikeCountis not a zero.- The pong is mandatory. The server pings on a fixed interval and
closes with
1011a socket it has not heard from for three ping intervals. A pong or any frame you send counts. Browser and Nodewsclients answer pings automatically; a hand-rolled client must reply itself. A socket killed mid-path by a proxy stays open and silent at both ends, and the pong is the only thing that detects it.
Before initial billing completes, the server queues at most 32 frames or 64 KiB
of application frames. Exceeding either limit sends an error frame with
initialization queue limit exceeded and closes with 1009. A single frame
larger than 64 KiB also closes with 1009.
Close codes
| code | meaning |
|---|---|
1001 | the server is going away; reconnect and resubscribe |
1008 | the key is out of quota |
1009 | a frame exceeds 64 KiB or the initialization queue exceeds its limits |
1011 | no pong or frame from you for three ping intervals |
1013 | frames are queueing faster than you read them |
Billing
This path is metered by the minute held open rather than by the call, from
the same credits as your REST calls. A minute starts when the socket opens,
and the meter stops when it closes. See GET /v2/me for your remaining
balance.
The price is per socket, not per token: one address, fifty, or every token all read the same on the meter.
Price: 20,000 CU per minute held open.
curl --request GET \
--url https://api.fomoscan.sh/v2/ws \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.fomoscan.sh/v2/ws', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.fomoscan.sh/v2/ws"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)Authorizations
B2B API key (fsk_live_… / fsk_test_…), issued by FomoScan. Also accepted via X-Api-Key.
Response
Switching Protocols — the socket is open.
