Skip to main content

WebSocket

A real-time feed of tracked-wallet trades. As each tracked wallet buys or sells, the trade is pushed to you within seconds.

wss://api.prysmatic-sol.xyz/ws
Credits are spent on delivery

Every payload the server sends you costs 1 credit, charged the moment it is delivered, whether or not your client reads or processes it successfully. Use the wallet filter below to avoid paying for trades you do not care about.

Connecting

Authenticate with the same Bearer header used by REST. Do not put API keys in the URL, where they can be captured by proxy or access logs:

Authorization: Bearer <API_KEY>

When the connection opens, the server sends a free control message describing the authenticated session:

{
"type": "ready",
"auth": "api_key",
"auth_method": "authorization",
"channels": ["trades"],
"credits": 1200,
"metered": true
}

The connection is refused if the key is unknown or the balance is zero.

import asyncio, json, websockets

API_KEY = "<API_KEY>"

async def main():
async with websockets.connect(
"wss://api.prysmatic-sol.xyz/ws",
extra_headers={"Authorization": f"Bearer {API_KEY}"},
) as ws:
await ws.send(json.dumps({"action": "subscribe", "channels": ["trades"]}))
async for raw in ws:
msg = json.loads(raw)
if msg.get("channel") == "trades":
print(msg["data"])

asyncio.run(main())

If you use websockets 15+, the header parameter is named additional_headers instead of extra_headers.

Subscribing

Send a subscribe message for the trades channel:

{ "action": "subscribe", "channels": ["trades"] }

The server confirms the active subscription:

{
"type": "subscribed",
"channels": ["trades"],
"rejected_channels": [],
"wallets": [],
"limits": { "max_channels": 8, "max_wallets": 100 }
}

Filtering by wallet

You usually do not want every wallet, and you do not want to pay for trades you do not care about. Add a wallets list (aliases from the Wallets API) and you will only receive, and only be charged for, trades from those wallets:

{ "action": "subscribe", "channels": ["trades"], "wallets": ["W12", "W37", "W205"] }
  • Omit wallets (or send an empty list) to receive all tracked wallets.
  • Re-sending subscribe replaces the filter, so you can change your watchlist at any time.

A trade from a wallet outside your filter is never delivered and never costs credits.

Unsubscribing

{ "action": "unsubscribe", "channels": ["trades"] }

The server replies with:

{ "type": "unsubscribed", "channels": ["trades"] }

Trade payload

{
"channel": "trades",
"data": {
"block_time": 1781404511,
"wallet": "W240",
"side": "buy",
"mint": "4Sm4YsjRrvSRQ4x1P86ncqumV3ZVgXxtFMKjiL8vpump",
"token_amount": "20415437641764",
"decimals": 6,
"sol_amount": 4.063765757,
"quote_mint": "So11111111111111111111111111111111111111112",
"quote_amount": "4063765757",
"program": "pump.fun"
}
}
  • wallet is a wallet alias.
  • sol_amount is already in SOL (not lamports).

Credits and limits

  • Each delivered payload consumes 1 credit.
  • Only the trades channel is available to API-key clients.
  • One active connection per API key. If you open a second one with the same key, the first is dropped with {"type":"superseded"}.
  • When your balance is spent, the server sends {"type":"balance_exhausted"} and closes the connection. Top up from the dashboard and reconnect.

Heartbeat

After a period of inactivity the server sends a heartbeat so it can detect and drop dead connections (and free the slot for other clients):

{ "type": "ping" }

You do not need to reply — receiving it is enough to keep your connection healthy, and a ping is free (it does not consume credits). Just ignore any message whose type is not a channel payload, as the example below does:

async for raw in ws:
msg = json.loads(raw)
if msg.get("channel") == "trades":
print(msg["data"])
# control messages ({"type": "ready" | "subscribed" | "ping" | "error" |
# "superseded" | "balance_exhausted"})
# are handled separately; "ping" can simply be ignored.

Limits

  • Message size: a frame you send larger than 4 KB closes the connection (1009).
  • Subscribe lists: at most 8 channels and 100 wallets per subscribe; anything beyond that is dropped.
  • Connections: one active connection per API key (see above), and the server has an overall connection cap. If it is full, new connections are closed with 1013.

Close codes

CodeMeaningWhat to do
1000Normal close.Reconnect if you still want the feed.
1008Policy: unknown/zero-balance key, or a control close (superseded, balance_exhausted).Do not blindly reconnect — see below.
1009A message you sent was too large.Fix the client; reconnect.
1013Server at capacity.Back off and retry later.

The control messages tell you why a 1008 is coming:

  • {"type": "balance_exhausted"} — you are out of credits. Top up, then reconnect. Reconnecting with a zero balance is refused.
  • {"type": "superseded"} — another connection opened with the same key and took over. Do not reconnect a duplicate; you would just kick the other one.

Reconnecting

Network drops happen. When the connection closes unexpectedly (not balance_exhausted or superseded), reconnect with exponential backoff and jitter rather than a tight loop, and cap the delay:

import asyncio, random, json, websockets

API_KEY = "<API_KEY>"

async def run():
delay = 1.0
while True:
try:
async with websockets.connect(
"wss://api.prysmatic-sol.xyz/ws",
extra_headers={"Authorization": f"Bearer {API_KEY}"},
) as ws:
await ws.send(json.dumps({"action": "subscribe", "channels": ["trades"]}))
delay = 1.0 # reset after a successful connect
async for raw in ws:
msg = json.loads(raw)
if msg.get("type") == "balance_exhausted":
return # stop: top up before reconnecting
if msg.get("channel") == "trades":
handle(msg["data"])
except Exception:
await asyncio.sleep(delay + random.uniform(0, delay)) # backoff + jitter
delay = min(delay * 2, 60)

Re-subscribe (and re-send your wallets filter) on every reconnect — subscriptions do not survive a dropped connection.