Live streams
Get live trades, PnL, wallet balances and task changes over one WebSocket with your API key.
A WebSocket is a connection that stays open, so the server can push news to you the moment it happens. Use it instead of asking a REST route again and again.
Before you start#
- Connect to
wss://api.vortexdeployer.com/ws. API keys work only at this address. - Your key needs the right scope for each topic. See Topics.
- Run this from a server. Never open a keyed socket in a web page: anyone could read the key there.
Connect and subscribe#
Every message is one JSON object. Put your key in token on each subscribe.
const ws = new WebSocket('wss://api.vortexdeployer.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', topic: 'txs:7xKX…pump', token: 'vd_…' }));
setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 25_000);
};
ws.onmessage = (m) => console.log(JSON.parse(m.data));What you send:
{ "type": "subscribe", "topic": "…", "token": "vd_…" }— start a topic. The answer is{ "type": "subscribed", "topic": "…" }, then the topic's first frame.{ "type": "unsubscribe", "topic": "…" }— stop a topic. The answer is{ "type": "unsubscribed", "topic": "…" }.{ "type": "ping" }— the server answers{ "type": "pong" }. Use it to check the line is alive.
When you connect, the server sends { "type": "welcome", "topics": [ … ] }. That list also holds topics the app uses. Only the topics below take an API key.
A refused subscribe answers { "type": "error", "topic": "…", "message": "…" }. The message says why, such as a missing scope.
Topics#
| Topic | Scope | What you get |
|---|---|---|
txs | stream | Every trade the platform sees, on every token. |
txs:<mint> | stream | Trades on one token. |
pnl | stream | Your PnL on every token, on Solana. |
pnl:<mint> | stream | Your PnL on one token. |
wallet:<address> | stream | The live balance of one of your wallets. |
tasks | read | Your running tasks, then every task change. |
user | read | A nudge when something in your account changed. |
<mint> is the token's mint address. Any other topic with a key, like xid, is refused.
Trades#
txs sends every trade as it lands. It starts with { "type": "txs_snapshot", "topic": "txs", "note": "firehose: deltas only" }, then one trade frame per trade.
txs:<mint> starts with { "type": "trade_snapshot", "topic": "txs:<mint>", "mint": "…", "trades": [ … ] }. That list holds the token's recent trades. Then new trades follow.
{
"type": "trade",
"chain": "solana",
"mint": "NnLz6wXfB3c41KwPWPvk9Rz75zR5enTGtYmsELrpump",
"quote_mint": "So11111111111111111111111111111111111111112",
"pool_address": "B8GeyGLChfj3Mn9HCyVJYS4nMRSMVDCCutRTwGasoH7V",
"dex": "pump_swap",
"side": "buy",
"trader": "A2uqWi4Zm3yVYqvuRb4GF6BSb56pocvw1J2V2nQeU3QE",
"base_amount": "44917",
"quote_amount": "179054",
"signature": "42RU7t2Drzyr…faT22",
"block": 447482805,
"block_index": 805,
"timestamp": 1789549114,
"pre_base_reserves": "2105102154648",
"pre_quote_reserves": "8391614579937",
"post_base_reserves": "2105102109731",
"post_quote_reserves": "8391614758991"
}Frames keep the chain feed's names, like pool_address and block_index. Only REST answers use camelCase.
| Field | Meaning |
|---|---|
mint | The token that was traded. |
quote_mint | What it was traded against, like wrapped SOL. |
pool_address, dex | The pool and the venue, like pump_swap. |
side | buy or sell. |
trader | The wallet that traded. |
base_amount, quote_amount | The two amounts, as strings in base units. |
signature, block, block_index, timestamp | Where it landed on the chain. timestamp is in Unix seconds. |
pre_…_reserves, post_…_reserves | What the pool held just before and just after. null when the venue does not say. |
The reserves let you work out the pool's price without another call.
PnL#
A pnl:<mint> frame:
{ "type": "pnl_update", "topic": "pnl:7xKX…pump", "mint": "7xKX…pump", "at": 1757923203112, "pnl": { } }pnlhas the same fields asGET /pnl?mint=. It isnullwhen the token cannot be priced, andreasonsays why.- On
pnlwith no mint, the frame haschainandtokensinstead.tokensmatchesGET /pnl. atis a time in milliseconds. It only goes up. Drop a frame whoseatis not newer than the last one.- A frame comes at most once every 3 seconds per topic.
- A frame with
errormeans the PnL cannot be worked out right now.
Wallet balances#
wallet:<address> must name one of your own wallets. A key with a wallet list can open only wallets on that list. Any other address is refused.
You get a wallet_balance_snapshot first, then a wallet_balance_update each time the balance changes:
{
"type": "wallet_balance_update",
"chain": "solana",
"address": "7tXi4mbvppPBgYgTVj2NpYq9K1KYYQvHv6tfCpJv9kMT",
"sol": "420100000",
"tokens": [ { "mint": "7xKX…pump", "amount": "3500000000" } ],
"block": 447482806,
"block_index": 806
}sol— the coin balance as a string in base units: lamports on Solana, wei on an EVM chain.tokens— the token balances it tracks, eachmintandamountin base units.block,block_index— where the balance was last seen.block_indexcan benull.
If no balance is saved yet, you get { "type": "wallet_balance_pending", "topic": "…" } first. The server reads the balance and sends an update.
To track one token from the start, add mint to the subscribe: { "type": "subscribe", "topic": "wallet:<address>", "mint": "7xKX…pump", "token": "vd_…" }.
Tasks#
tasks starts with your running and paused tasks:
{ "type": "task_snapshot", "topic": "tasks", "tasks": [ { "task_id": "6186", "task_type": "buy", "mint": "7xKX…pump", "status": "running", "label": "morning buys", "updated_at": "2026-09-16T01:09:24.531Z" } ] }Then it sends three kinds of frames:
task_update— a task's status changed. It hastask_id,task_type,mint,status,last_errorandupdated_at.task_execution— a task sent a trade. It hastask_idand anexecutionwithid,side,wallet_id,amount_in,amount_out,signature,confirmed,err,sourceandcreated_at.wallet_skipped— a task left a wallet out. It hastask_id,wallet_id,mint,reason,sourceandtimestamp.
See Tasks API for what each status means.
Account nudges#
user starts with { "type": "user_snapshot", "topic": "user", "note": "deltas only; re-read your REST resources now" }. Read what you need over REST at that point.
After that you get:
{ "type": "invalidate", "resource": "wallets", "id": null }— that part of your account changed. Read it again over REST.resourcenames it, likewalletsorportfolio.{ "type": "launch", … }— a launch moved on.stageisprepared,confirmed,unsettledorfailed. The frame also haslaunch_id,project_id,mint,signatureanderr.
Rules and limits#
- One key per socket. After the first keyed subscribe, a different key on the same socket is refused.
- 5 sockets per key. A sixth socket gets an error on its first subscribe.
- 50
txs/pnltopics and 50wallettopics per socket. - 4 subscribes waiting at once. Wait for answers before you send more.
- The key's IP list applies to sockets too.
- A message you send can be up to 4,096 bytes.
- Keys cannot use the socket for reads. Use REST.
These close the socket:
| Code | Why |
|---|---|
4001 | The key was revoked, or its settings changed. Reconnect with a valid key. |
4008 | 10 subscribes failed with a bad key. The next subscribe closes the socket. |
1013 | Too many sockets from your IP address. |
Slow readers and reconnects#
If you read too slowly, the server drops frames for you. When you catch up, it sends { "type": "resync", "topic": "…" } and then a fresh first frame for that topic. Treat it like a new start and read missed state over REST.
A drop loses your topics. Reconnect with a short, growing wait, then subscribe again.