Skip to main content
WSS
/
ws
/
swaps
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

NeedUse
Browser app, dashboardWebSocket
Low-to-mid throughput serverWebSocket
Highest throughput, schema-typed clientsgRPC
Live OHLCV barsWebSocket: OHLCV
Pump.fun mint / migration eventsThis socket — join the latest / graduating / graduated rooms
One-shot historical dataREST

Endpoint and auth

wss://ws.dexploit.dev/ws/swaps
Authenticate either via Bearer header (server) or query string (browser):
Authorization: Bearer ohlcv_live_sk_<your_key>

# or, for browsers where setting headers is awkward:
wss://ws.dexploit.dev/ws/swaps?api_key=ohlcv_live_sk_<your_key>

Subscribe

After the connection is up, send a subscribe message. Filters are optional — empty filters mean “every swap on every pool”.
{
  "type": "subscribe",
  "filters": {
    "pools": ["<pool_address>"]
  }
}
You’ll receive:
{ "type": "subscribed", "message": "Subscribed: pools" }
Then swap messages flow as trades happen.

Supported filters

All filters are optional and combine as AND. Set to null or omit to disable.
KeyTypeNotes
poolsstring[]Filter by pool address (this is the most common filter — see Pairs vs tokens).
tokensstring[]Filter by token mint.
tradersstring[]Filter by trader wallet.
dexesstring[]Values: pumpfun, pumpswap, raydium_amm, raydium_clmm, raydium_cpmm, orca, meteora_damm_v2, meteora_dbc, meteora_dlmm, meteora_pools.
min_solnumber (lamports)Minimum SOL amount. 1 SOL = 1,000,000,000 lamports.
max_solnumber (lamports)Maximum SOL amount.
is_buybooleantrue for buys only, false for sells only.

Wire format gotchas

A swap frame looks like this (captured live from meteora_damm_v2):
{
  "type": "swap",
  "signature": "3rjMq3C7Zp4eDDRtKwEBmVFXErFt4twvUVryNerBgkrxqs53XhAgL3LqDvfiZjDLNHEbwkbkQMTwfSjD1BvSboES",
  "slot": 420984167,
  "timestamp": 1779279483,
  "dex": "meteora_damm_v2",
  "swap_type": "sell",
  "trader": "rk4jdi7srE6VKtxPeCKnqpEe7uJdZLLdpv1qAPK8LRd",
  "token_mint": "8ZiqYN6rEm6PapbhCm5wWWzkRjsYyixgqW3K2384D1C3",
  "pool_address": "DT75gs9fygQxi2Aq2ST1jz9C6FTNcbBjpgSpdDpMDdLX",
  "amount_in": 350832255298,
  "amount_out": 729873626,
  "sol_amount": 729873626,
  "token_amount": 350832255298,
  "virtual_sol_reserves": null,
  "virtual_token_reserves": null,
  "real_sol_reserves": 91985332910,
  "real_token_reserves": 44561454812398,
  "lp_fee_bps": 1,
  "protocol_fee_bps": 0,
  "fee": null,
  "creator_fee": null,
  "creator_fee_bps": 0,
  "price_impact_bps": null,
  "base_decimals": 9,
  "quote_decimals": 6,
  "pool_price": 2.0642354092175554e-6,
  "transfer_fee_in": 0,
  "transfer_fee_out": 0,
  "cashback": null,
  "cashback_bps": 0,
  "maker_tags": []
}
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.
{
  "type": "pool_event",
  "event_type": "lp_withdraw",
  "timestamp_ms": 1717000000000,
  "slot": 270123456,
  "signature": "5Qx…",
  "user": "9aB…",
  "pool_address": "DT7…",
  "token_mint": "8Zi…",
  "base_amount": 123456789,
  "quote_amount": 4200000000,
  "lp_token_amount": 987654321,
  "drained_pct": 0.97,
  "pool_base_reserve_after": 0,
  "pool_quote_reserve_after": 0,
  "lp_supply_after": 30000,
  "dex": 2
}

lp_withdraw / lp_deposit (LpEvent) fields

FieldTypeNotes
timestamp_msi64Epoch milliseconds. ⚠ The swap frame on this same socket uses timestamp in epoch seconds — a 1000× difference. Don’t mix them.
slotu64Solana slot.
signaturestringBase58 transaction signature.
userstringWallet that added/removed the LP.
pool_addressstringOn-chain pool / LP account (elsewhere called pair_address).
token_mintstringToken mint.
base_amountu64Memecoin side, atomic units. Divide by 10**decimals (~6 for pump tokens). ⚠ Decimals are not carried on the event.
quote_amountu64The SOL / WSOL side in lamports, despite no _sol suffix. ⚠ Divide by 1e9 for SOL (the lamports-named-non-_sol trap).
lp_token_amountu64LP 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_pctf32 (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_afteru64Post-event pool reserve, base side, atomic. 0 when N/A.
pool_quote_reserve_afteru64Post-event pool reserve, quote side, lamports. 0 when N/A.
lp_supply_afteru64Post-event total LP supply. Constant-product only — 0 for Meteora.
dexint (Enum8)2=PumpSwap, 3=RaydiumAmm, 4=RaydiumClmm, 5=RaydiumCpmm, 6=Orca, 7=MeteoraDammV2, 9=MeteoraDlmm.
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:
{
  "type": "pool_event",
  "event_type": "token_burn",
  "timestamp_ms": 1717000001000,
  "slot": 270123457,
  "signature": "3rj…",
  "user": "9aB…",
  "token_mint": "8Zi…",
  "amount_atomic": 999999999999
}
FieldTypeNotes
timestamp_msi64Epoch milliseconds (same as LpEvent).
slotu64Solana slot.
signaturestringBase58 transaction signature.
userstringWallet that burned.
token_mintstringToken mint.
amount_atomicu64Burned 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.
{
  "type": "pool_event",
  "event_type": "rug_detected",
  "timestamp_ms": 1717000002000,
  "slot": 270123458,
  "signature": "5Qx…",
  "pool_address": "DT7…",
  "token_mint": "8Zi…",
  "dex": 2,
  "lp_sol_pulled": 4200000000,
  "drained_pct": 0.97,
  "classification": "RUGGED",
  "reason": "lp_withdraw",
  "graduated": true
}
FieldTypeNotes
timestamp_msi64Epoch milliseconds.
slotu64Solana slot.
signaturestringBase58 transaction signature of the triggering withdraw/burn.
pool_addressstringOn-chain pool / LP account.
token_mintstringToken mint.
dexint (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_pulledu64SOL liquidity removed, in lamports. Divide by 1e9 for SOL.
drained_pctf32 (0..1)Fraction of pool liquidity removed.
classificationstring"RUGGED" when >95% of liquidity was removed; "LIQUIDITY_DRAINING" when 50–95%.
reasonstring"lp_withdraw" or "lp_burn" — which action triggered the classification.
graduatedboolWhether 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, 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:
{ "type": "join", "room": "transaction:<mint>" }
(or transaction:<mint>:<pool> to scope to one pool). Events then arrive as:
{
  "type": "message",
  "room": "transaction:<mint>",
  "data": { "event_type": "lp_withdraw", "timestamp_ms": 1717000000000, "...": "..." }
}
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).
RoomJoin stringWhat it carries
Latest mintslatestNew-mint discovery — one data per mints.observed event as a token is first seen.
GraduatinggraduatingTokens crossing the bonding-curve completion threshold (the mints.completing lifecycle event).
GraduatedgraduatedTokens that migrated to a tradeable AMM pool (the mints.graduated lifecycle event).
Rugs (global)rugsEvery rug_detected across all venues — the full RugRecord (classification, drained_pct, lp_sol_pulled in lamports, dex, plus reason + graduated). See below.
Token safetysafety:{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 barsprice:{addr}:{tf}Live OHLCV bar updates for addr (a mint OR pool) at timeframe tf. tf1s 30s 1m 5m 15m 1h 4h 1d. A mint-keyed room interleaves the mint’s pairs (each frame carries pair_address).
Wallet tradeswallet:{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 PnLpnl:{wallet}Per-position PnL frames for wallet — one frame per holding, re-pushed as the wallet trades or spot moves. Pro+.
Position PnLpnl:{wallet}:{token}A single (wallet, token) position, glass-box (with realized_breakdown). Pro+. A token literally named summary is not addressable.
Wallet PnL summarypnl:{wallet}:summaryThe 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

{ "type": "join", "room": "rugs" }
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:
{
  "type": "message",
  "room": "rugs",
  "data": {
    "event_type": "rug_detected",
    "token_mint": "8Zi…",
    "pool_address": "DT7…",
    "dex": 7,
    "classification": "RUGGED",
    "drained_pct": 0.97,
    "lp_sol_pulled": 4200000000,
    "reason": "lp_withdraw",
    "graduated": true
  }
}

A safety:{mint} frame

{
  "type": "message",
  "room": "safety:8Zi…",
  "data": { "mint": "8Zi…", "holder_count": 412, "top10_pct": 0.38, "dev_held_pct": 0.0, "sniper_count": 9, "insider_count": 3, "bundler_count": 0, "...": "..." }
}

A pnl:{wallet}:summary frame

{
  "type": "message",
  "room": "pnl:9aB…:summary",
  "data": {
    "wallet": "9aB…",
    "pnl_mode": "adjusted",
    "realized_sol": 12.34,
    "unrealized_sol": -1.2,
    "cost_basis_sol": 40.0,
    "open_positions": 5,
    "closed_positions": 18,
    "positions": [ { "mint": "8Zi…", "realized_sol": 3.1, "unrealized_sol": 0.4, "...": "..." } ]
  }
}

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).
DEXdexLP / rug events
PumpSwap2✅ Live
RaydiumAmm v43✅ Live
RaydiumClmm4✅ Live
RaydiumCpmm5✅ Live
Orca6✅ Live
MeteoraDammV27✅ Live
MeteoraDlmm9✅ Live
MeteoraDbc8❌ Not emitted
MeteoraPools10❌ Not emitted
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

import WebSocket from 'ws'; // browser: use the global WebSocket

const URL = 'wss://ws.dexploit.dev/ws/swaps';
const API_KEY = 'ohlcv_live_sk_<your_key>';
const POOL = '<pool_address>';

let backoff = 1000;

function connect() {
  const ws = new WebSocket(URL, {
    headers: { Authorization: `Bearer ${API_KEY}` }
  });

  ws.on('open', () => {
    backoff = 1000; // reset on successful connect
    ws.send(JSON.stringify({
      type: 'subscribe',
      filters: { pools: [POOL] }
    }));
  });

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    switch (msg.type) {
      case 'subscribed':
        console.log('subscribed:', msg.message);
        break;
      case 'swap': {
        const sol = msg.sol_amount / 1e9;
        const tok = msg.token_amount / Math.pow(10, msg.quote_decimals ?? 6);
        const price = sol / tok;
        console.log(
          `${msg.swap_type.toUpperCase()} ${tok.toFixed(2)} @ ${price.toExponential(3)} SOL ` +
          `(${msg.dex}, sig ${msg.signature.slice(0, 8)}…)`
        );
        break;
      }
      case 'error':
        console.error('stream error:', msg.message);
        break;
    }
  });

  ws.on('close', () => {
    console.warn(`disconnected, retrying in ${backoff}ms`);
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, 60_000);
  });

  ws.on('error', (err) => {
    console.error('ws error:', err);
    // 'close' fires next; reconnect happens there.
  });
}

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

Server messages

typeWhen
subscribedAfter your subscribe.
unsubscribedAfter your unsubscribe.
swapA trade matched your filters.
pongReply to your ping.
errorSubscribe 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
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.