Messages
Streaming
WebSocket: swaps
Real-time swap stream over WebSocket. Subscribe by pool, get pushes as trades happen.
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
Authenticate either via Bearer header (server) or query string (browser):
You’ll receive:
Then
A few things worth flagging:
There is no buy/sell side and no price field on a pool event — it’s a liquidity action, not a trade.
A burn carries a different field set. There is no
(or
The global
The
A
A
For production hardening (resubscribe state, gap-filling via REST, idle pings), see Reconnect & backpressure.
latest, graduating, and graduated discovery rooms.
When to use WebSocket
| Need | Use |
|---|---|
| Browser app, dashboard | WebSocket |
| Low-to-mid throughput server | WebSocket |
| Highest throughput, schema-typed clients | gRPC |
| Live OHLCV bars | WebSocket: OHLCV |
| Pump.fun mint / migration events | This socket — join the latest / graduating / graduated rooms |
| One-shot historical data | REST |
Endpoint and auth
Subscribe
After the connection is up, send asubscribe message. Filters are optional — empty filters mean “every swap on every pool”.
swap messages flow as trades happen.
Supported filters
All filters are optional and combine as AND. Set tonull or omit to disable.
| Key | Type | Notes |
|---|---|---|
pools | string[] | Filter by pool address (this is the most common filter — see Pairs vs tokens). |
tokens | string[] | Filter by token mint. |
traders | string[] | Filter by trader wallet. |
dexes | string[] | Values: pumpfun, pumpswap, raydium_amm, raydium_clmm, raydium_cpmm, orca, meteora_damm_v2, meteora_dbc, meteora_dlmm, meteora_pools. |
min_sol | number (lamports) | Minimum SOL amount. 1 SOL = 1,000,000,000 lamports. |
max_sol | number (lamports) | Maximum SOL amount. |
is_buy | boolean | true for buys only, false for sells only. |
Wire format gotchas
Aswap frame looks like this (captured live from meteora_damm_v2):
swap_typeis the string"buy"or"sell"(lowercase). Notis_buy. (is_buyis only a filter key, not a field on the event.) The REST/swaps*endpoints useis_buy(bool) instead — these two surfaces are intentionally different.dexis the string name here. On the REST/swaps*endpointsdexis an integer ID. Same enum, different encoding.timestampis epoch seconds. On REST/swaps*it’s epoch milliseconds; on REST candles it’s ISO 8601.- There is no
pricefield. Usepool_price(decimals-adjusted mid after the swap) or compute fill price client-side:price = sol_amount / token_amount, accounting forbase_decimalsandquote_decimals. - Amounts are integers in base units.
sol_amountis in lamports (1 SOL = 1e9).token_amountis in token base units; divide by10 ** quote_decimalsto get the human-readable amount. pool_addressis what we elsewhere callpair_address— same concept (the on-chain pool/LP account). The swaps stream keys it aspool_address; REST and the OHLCV WebSocket key the same account aspair_address.maker_tagscarries the wallet-intel classification of the trader at swap time (sniper,insider,whale, …). Useful for live alerting on smart-money buys.- Nullable fields.
virtual_*_reservesare non-null only on pump.fun.fee,creator_fee,price_impact_bps,cashbackcome back null on protocols that don’t report them — fall back to the_bpssiblings.
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 asubscribe 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
| Field | Type | Notes |
|---|---|---|
timestamp_ms | i64 | Epoch milliseconds. ⚠ The swap frame on this same socket uses timestamp in epoch seconds — a 1000× difference. Don’t mix them. |
slot | u64 | Solana slot. |
signature | string | Base58 transaction signature. |
user | string | Wallet that added/removed the LP. |
pool_address | string | On-chain pool / LP account (elsewhere called pair_address). |
token_mint | string | Token mint. |
base_amount | u64 | Memecoin side, atomic units. Divide by 10**decimals (~6 for pump tokens). ⚠ Decimals are not carried on the event. |
quote_amount | u64 | The SOL / WSOL side in lamports, despite no _sol suffix. ⚠ Divide by 1e9 for SOL (the lamports-named-non-_sol trap). |
lp_token_amount | u64 | LP tokens, atomic. ⚠ Meteora DAMM v2 (dex=7) saturates this to u64::MAX on high-liquidity pools — use base_amount + quote_amount for value, never lp_token_amount. |
drained_pct | f32 (0..1) | Fraction of pool liquidity removed by this event. Constant-product pools (PumpSwap, RaydiumAmm) use the LP-supply ratio; Meteora CL uses the SOL-reserve ratio. 0.0 when not computable. |
pool_base_reserve_after | u64 | Post-event pool reserve, base side, atomic. 0 when N/A. |
pool_quote_reserve_after | u64 | Post-event pool reserve, quote side, lamports. 0 when N/A. |
lp_supply_after | u64 | Post-event total LP supply. Constant-product only — 0 for Meteora. |
dex | int (Enum8) | 2=PumpSwap, 3=RaydiumAmm, 4=RaydiumClmm, 5=RaydiumCpmm, 6=Orca, 7=MeteoraDammV2, 9=MeteoraDlmm. |
token_burn (BurnRecord) — a different shape
A burn carries a different field set. There is no pool_address and no base/quote amount:
| Field | Type | Notes |
|---|---|---|
timestamp_ms | i64 | Epoch milliseconds (same as LpEvent). |
slot | u64 | Solana slot. |
signature | string | Base58 transaction signature. |
user | string | Wallet that burned. |
token_mint | string | Token mint. |
amount_atomic | u64 | Burned amount in atomic units. |
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.
| Field | Type | Notes |
|---|---|---|
timestamp_ms | i64 | Epoch milliseconds. |
slot | u64 | Solana slot. |
signature | string | Base58 transaction signature of the triggering withdraw/burn. |
pool_address | string | On-chain pool / LP account. |
token_mint | string | Token mint. |
dex | int (Enum8) | The venue the drain happened on — any non-pumpfun AMM/CLMM (e.g. 2=PumpSwap, 3=RaydiumAmm, 4=RaydiumClmm, 5=RaydiumCpmm, 6=Orca, 7=MeteoraDammV2, 9=MeteoraDlmm). |
lp_sol_pulled | u64 | SOL liquidity removed, in lamports. Divide by 1e9 for SOL. |
drained_pct | f32 (0..1) | Fraction of pool liquidity removed. |
classification | string | "RUGGED" when >95% of liquidity was removed; "LIQUIDITY_DRAINING" when 50–95%. |
reason | string | "lp_withdraw" or "lp_burn" — which action triggered the classification. |
graduated | bool | Whether the mint had already graduated/migrated off the pump.fun bonding curve when the drain landed. The graduation/migration window itself is excluded, so a rug_detected is a post-migration (or never-pumped) AMM drain. |
Filter scope for pool events
In filter-mode, onlytokens 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 (ajoin message) instead of filters, the same pool events arrive wrapped in a room message. Join a transaction room:
transaction:<mint>:<pool> to scope to one pool). Events then arrive as:
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).
| Room | Join string | What it carries |
|---|---|---|
| Latest mints | latest | New-mint discovery — one data per mints.observed event as a token is first seen. |
| Graduating | graduating | Tokens crossing the bonding-curve completion threshold (the mints.completing lifecycle event). |
| Graduated | graduated | Tokens that migrated to a tradeable AMM pool (the mints.graduated lifecycle event). |
| Rugs (global) | rugs | Every rug_detected across all venues — the full RugRecord (classification, drained_pct, lp_sol_pulled in lamports, dex, plus reason + graduated). See below. |
| Token safety | safety:{mint} | The mint’s full actor/safety snapshot (holder/sniper/insider/bundler counts, top-holder %, dev-held %, …), re-pushed on-change (~3–5 s coalesced). |
| Price bars | price:{addr}:{tf} | Live OHLCV bar updates for addr (a mint OR pool) at timeframe tf. tf ∈ 1s 30s 1m 5m 15m 1h 4h 1d. A mint-keyed room interleaves the mint’s pairs (each frame carries pair_address). |
| Wallet trades | wallet:{wallet} | Every swap where wallet is the trader (a live per-wallet trade tape). |
| Transaction (token) | transaction:{mint} | Interleaved swap + lp_deposit/lp_withdraw/token_burn + rug_detected for one mint (all pools). |
| Transaction (pool) | transaction:{mint}:{pool} | Same as above, scoped to a single pool. |
| Wallet PnL | pnl:{wallet} | Per-position PnL frames for wallet — one frame per holding, re-pushed as the wallet trades or spot moves. Pro+. |
| Position PnL | pnl:{wallet}:{token} | A single (wallet, token) position, glass-box (with realized_breakdown). Pro+. A token literally named summary is not addressable. |
| Wallet PnL summary | pnl:{wallet}:summary | The wallet-level rollup (realized_sol, unrealized_sol, cost_basis_sol, open_positions, closed_positions, positions[]). Pro+. |
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
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).| DEX | dex | LP / rug events |
|---|---|---|
| PumpSwap | 2 | ✅ Live |
| RaydiumAmm v4 | 3 | ✅ Live |
| RaydiumClmm | 4 | ✅ Live |
| RaydiumCpmm | 5 | ✅ Live |
| Orca | 6 | ✅ Live |
| MeteoraDammV2 | 7 | ✅ Live |
| MeteoraDlmm | 9 | ✅ Live |
| MeteoraDbc | 8 | ❌ Not emitted |
| MeteoraPools | 10 | ❌ Not emitted |
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) — recentrug_detectedevents across all venues (7-day window, paged; requires an API key).drained_pctis a 0..1 fraction andlp_sol_pulledis already in SOL — butreason/graduatedare WS-only (use therugsroom for those). - REST:
GET /tokens/{mint}/rugs— rug history for a single mint. - MCP: the
list_pool_eventstool 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
Server messages
type | When |
|---|---|
subscribed | After your subscribe. |
unsubscribed | After your unsubscribe. |
swap | A trade matched your filters. |
pong | Reply to your ping. |
error | Subscribe rejected, auth failed, or server-side error. |
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.Messages

