Skip to main content
WSS
The swaps WebSocket is the easiest way to get real-time DEX trades into a browser, dashboard, or low-volume server. For higher throughput, see gRPC. For live OHLCV bars, see OHLCV candles. Pump.fun mint / graduation events stream from this same socket — join the latest, graduating, and graduated discovery rooms.

When to use WebSocket

Endpoint and auth

Authenticate either via Bearer header (server) or query string (browser):

Subscribe

After the connection is up, send a subscribe message. Filters are optional — empty filters mean “every swap on every pool”.
You’ll receive:
Then swap messages flow as trades happen.

Supported filters

All filters are optional and combine as AND. Set to null or omit to disable.

Wire format gotchas

A swap frame looks like this (captured live from meteora_damm_v2):
A few things worth flagging:
  • swap_type is the string "buy" or "sell" (lowercase). Not is_buy. (is_buy is only a filter key, not a field on the event.) The REST /swaps* endpoints use is_buy (bool) instead — these two surfaces are intentionally different.
  • dex is the string name here. On the REST /swaps* endpoints dex is an integer ID. Same enum, different encoding.
  • timestamp is epoch seconds. On REST /swaps* it’s epoch milliseconds; on REST candles it’s ISO 8601.
  • There is no price field. Use pool_price (decimals-adjusted mid after the swap) or compute fill price client-side: price = sol_amount / token_amount, accounting for base_decimals and quote_decimals.
  • Amounts are integers in base units. sol_amount is in lamports (1 SOL = 1e9). token_amount is in token base units; divide by 10 ** quote_decimals to get the human-readable amount.
  • pool_address is what we elsewhere call pair_address — same concept (the on-chain pool/LP account). The swaps stream keys it as pool_address; REST and the OHLCV WebSocket key the same account as pair_address.
  • maker_tags carries the wallet-intel classification of the trader at swap time (sniper, insider, whale, …). Useful for live alerting on smart-money buys.
  • Nullable fields. virtual_*_reserves are non-null only on pump.fun. fee, creator_fee, price_impact_bps, cashback come back null on protocols that don’t report them — fall back to the _bps siblings.

Pool events (LP add/remove + burns)

The same /ws/swaps socket also carries non-swap pool activity — LP deposits, LP withdrawals, token burns, and a server-classified rug signal. An unfiltered connection (or one filtered only by tokens/pools) receives these frames interleaved with swaps. This is the real-time liquidity-removal push that lets a bot react to a rug in the same second it lands, instead of polling.
lp_withdraw is the live rug / LP-removal signal. A large lp_withdraw is liquidity leaving the pool the instant it happens. rug_detected is the classified version — the server applies a drain threshold and a graduation-exclusion gate and tells you RUGGED vs LIQUIDITY_DRAINING so you don’t have to.

Filter-mode frame

In filter-mode (the default — the mode you get after sending a subscribe message), pool events arrive with a top-level type: "pool_event" and an event_type discriminator. All four pool-event kinds share type: "pool_event" — demux on event_type.

lp_withdraw / lp_deposit (LpEvent) fields

There is no buy/sell side and no price field on a pool event — it’s a liquidity action, not a trade.

token_burn (BurnRecord) — a different shape

A burn carries a different field set. There is no pool_address and no base/quote amount:

rug_detected (RugRecord) — the classified signal

rug_detected is a server-side classified rug / drain signal, derived from an lp_withdraw or lp_burn. It fires on any non-pumpfun venue when a real LP drain crosses the drain threshold outside the graduation/migration window — i.e. on a genuine AMM/CLMM pool, not the pump.fun bonding curve (which has no fungible LP to pull). Pump graduation-migrations are excluded: they move liquidity but aren’t rugs, so they never produce a rug_detected.

Filter scope for pool events

In filter-mode, only tokens and pools apply to pool events. The dexes, min_sol, max_sol, traders, is_buy, and wallet_tags filters are swap-only and have no effect on pool-event delivery. A default (empty) subscription delivers the full pool-event firehose immediately on connect — you don’t have to subscribe to start receiving them.

Room-mode delivers the same events with a different shape

If you use rooms (a join message) instead of filters, the same pool events arrive wrapped in a room message. Join a transaction room:
(or transaction:<mint>:<pool> to scope to one pool). Events then arrive as:
Dual-frame trap. The same pool event has two on-wire shapes depending on mode. In filter-mode it’s top-level (type: "pool_event", event_type: …). In room-mode the data object carries event_type but not a top-level type. Read event_type from msg in filter-mode and from msg.data in room-mode.
transaction:{mint} rooms interleave swaps, pool events (lp_deposit/lp_withdraw/token_burn), and rug_detected — demux on type / event_type. Room-mode is mode-exclusive with filter-mode on a given connection: the first subscribe locks the socket into filter-mode, and the first join locks it into room-mode. Pick one per connection.

Rooms reference

Every valid /ws/swaps room is listed below. Join with { "type": "join", "room": "<room>" }; the server replies { "type": "joined", "room": "<room>" } (and { "type": "left", "room": "<room>" } after a leave). An unknown or malformed room is rejected with an error frame (unknown_room / malformed_room). Joining a room that isn’t the discovery rooms — i.e. any pnl:* room — requires the Pro or Enterprise tier; lower tiers get a tier_required error. Per-tier join caps apply: Free 5, Developer 25, Pro 100, Enterprise unlimited. Unless noted, every room delivers the room-mode wrapper{ "type": "message", "room": "<room>", "data": { … } } — and you read the real payload from data (the dual-frame trap above).
pnl:* rooms push an instant snapshot on join. The other rooms only start delivering on the next matching event; the PnL rooms seed the joining connection with its current book immediately (then keep it live).

The global rugs room

The rugs room fans out the full RugRecord for every rug_detected — the same object the rug_detected field table describes, including reason and graduated (and the dex venue id). This is a superset of what the REST GET /rugs feed (under Rugs in the API reference) returns — REST omits reason and graduated (they live only on the NATS wire), so use the room when you need them live:

A safety:{mint} frame

A pnl:{wallet}:summary frame

Per-DEX coverage matrix

LP and rug events are live for every AMM venue. The only two gaps are the bonding-curve venues, which have no fungible LP to add, remove, or drain (see Data coverage).
Absence is not safety on the two uncovered venues. meteora_dbc (8) is a bonding curve — there’s no fungible LP, so the curve is the liquidity and there’s nothing to lp_withdraw. meteora_pools (10) routes liquidity through dynamic vaults shared across many pools, so a per-pool LP add/remove isn’t derivable from a swap (see Data coverage). On those two, not seeing an LP-removal or rug_detected event is not proof a pool is safe.

Historical / replay companions

For backfill and gap-filling, the same events are queryable over REST and MCP:
  • REST: GET /pool-events?token=<mint>&limit=N&before=<ts_ms> — paginated LP/burn history (cursor is epoch ms).
  • REST: GET /rugs (under Rugs in the API reference) — recent rug_detected events across all venues (7-day window, paged; requires an API key). drained_pct is a 0..1 fraction and lp_sol_pulled is already in SOL — but reason/graduated are WS-only (use the rugs room for those).
  • REST: GET /tokens/{mint}/rugs — rug history for a single mint.
  • MCP: the list_pool_events tool wraps the same pool-events endpoint.
Edge: /ws/swaps is already routed at the OVH edge — both lp_withdraw and rug_detected ride this same socket. No new endpoint or connection is needed.

Complete TypeScript client

For production hardening (resubscribe state, gap-filling via REST, idle pings), see Reconnect & backpressure.

Server messages

Programmatic spec

If you’re generating clients or feeding the contract into an LLM/codegen pipeline, every WebSocket channel — /ws/swaps and /ws/ohlcv — is described as a single AsyncAPI 3.0 document. All message schemas and live-captured example payloads are included.
api_key
type:httpApiKey

?api_key=ohlcv_live_sk_… appended to the connection URL. Browser-friendly; the only option in environments where you can't set headers.

Set filters
type:object

Replaces the active filter set. Empty filters means "every swap on every pool".

Clear filters
type:object
Heartbeat
type:object

Optional keepalive. Server replies with pong. Recommended every 25–30 seconds.

Join room
type:object

Subscribe to a room (e.g. transaction:<mint> or transaction:<mint>:<pool>). First join locks room-mode.

Leave room
type:object
Swap event
type:object

One executed swap on a Solana DEX, normalized across protocols.

Pool event

Non-swap pool activity (filter-mode): LP deposit / withdraw, token burn, or the server-classified rug signal. All four share type: "pool_event" — demux on event_type. lp_withdraw is the real-time LP-removal signal; rug_detected is the classified version.

Room message
type:object

Room-mode delivery wrapper. The swap or pool event is nested under data; data carries event_type but NOT a top-level type (dual-frame trap).

Join ack
type:object

Sent after a successful join.

Leave ack
type:object
Connection / subscribe ack
type:object

Sent on connect AND after each successful subscribe. The connect-time frame includes a client-N ID for log correlation.

Unsubscribe ack
type:object
Pong
type:object

Reply to a client ping. Send a ping every ~25–30s to keep the connection from idling out.

Stream error
type:object

Validation failure, auth issue, or server-side error. Connection usually stays open after a recoverable error.