Pump.fun & PumpSwap WebSocket API: Real-Time Solana Market Data
Stream live Pump.fun and canonical PumpSwap activity from Solana — new launches, bonding-curve trades, AMM trades, migrations and wallet movements — within milliseconds of block confirmation. No polling, and no API key to start.
Endpoint: wss://pumpdev.io/ws
Authentication: none required (anonymous tier) — a free API key gives 5× the limits
Free forever: subscribeNewToken. Launches never count against any quota, on any tier.
A token subscription follows the token for its whole lifecycle — bonding curve, migration, then the canonical PumpSwap pool — with no re-subscribe. Other platforms and non-canonical pools are ignored by default.
The same account and key stream PONS launches and trades over the PONS WebSocket, and the PONS Trading API buys and sells them.
Quick start
import WebSocket from 'ws';
const ws = new WebSocket('wss://pumpdev.io/ws');
ws.on('open', () => {
ws.send(JSON.stringify({ method: 'subscribeNewToken' }));
ws.send(JSON.stringify({ method: 'subscribeTokenTrade', keys: ['TokenMint1'] }));
ws.send(JSON.stringify({ method: 'subscribeAccountTrade', keys: ['WalletAddress1'] }));
});
ws.on('message', (raw) => {
const event = JSON.parse(raw.toString());
if (event.type) return; // control frame
console.log(event.txType, event.mint, event.quoteAmount ?? event.quoteAmountRaw);
});
ws.on('error', (err) => console.error(err.message));
import asyncio, json, websockets # pip install websockets
async def main():
async with websockets.connect("wss://pumpdev.io/ws") as ws:
await ws.send(json.dumps({"method": "subscribeNewToken"}))
async for raw in ws:
event = json.loads(raw)
if event.get("txType") == "create":
print(event["name"], event["symbol"], event["mint"])
asyncio.run(main())
Subscriptions
| Method | Payload | Delivers |
|---|---|---|
subscribeNewToken | — | txType: "create" — every new Pump.fun launch |
subscribeTokenTrade | keys: [mint] | buy / sell / complete / create_pool for those mints |
subscribeAccountTrade | keys: [wallet] | buy / sell made by those wallets |
Each has a matching unsubscribe… method with the same payload. Token and
wallet subscriptions draw from one shared pool.
Keys are base58 — Solana mints and wallets. A key in another alphabet is
dropped from the batch and reported as TOKEN_SUBSCRIBE_NORMALIZED /
ACCOUNT_SUBSCRIBE_NORMALIZED:
{
"type": "error",
"code": "ACCOUNT_SUBSCRIBE_NORMALIZED",
"message": "Normalized account subscription batch: accepted=0, invalid=1, truncated=0 The rejected key is a Robinhood Chain (EVM) address: 0xDd37…2824. This endpoint is the Solana feed and takes base58 Solana addresses — subscribe it on wss://rhc.pumpdev.io/ws instead, using the same method.",
"reason": "WRONG_CHAIN_ENDPOINT",
"useEndpoint": "wss://rhc.pumpdev.io/ws",
"invalidKeys": ["0xDd3764EF180f46F32EB0AFd4baf4c9942a042824"],
"expectedKeyFormat": "base58 Solana addresses"
}
Read the three counts first, because they separate two different problems:
invalid is a key this feed cannot parse, and truncated is a batch clipped by
your plan's batch size. A tier never shows up as invalid — if truncated
is 0, your plan is not involved, whatever tier you are on. reason is
WRONG_CHAIN_ENDPOINT when the keys are well-formed but belong to our other
feed, and useEndpoint names the URL that would have taken them.
The server acknowledges every call, listing the keys actually applied — diff them against what you sent to detect partial acceptance:
{ "type": "subscribed", "method": "subscribeTokenTrade", "keys": ["TokenMint1"] }
On connect you also get {"type":"connected"} and
{"type":"connectionStatus","connected":true,"timestamp":…}. The latter reports
our upstream feed's health and is re-sent whenever it changes; it says nothing
about your API key (that is the auth frame below).
Quote-aware amounts: read quoteMint first
Every market event names its quote mint, and not every market is quoted in SOL.
- Native-SOL pairs —
quoteAmount,marketCapQuote,solAmountandmarketCapSolare normalized and ready to use. - Non-SOL pairs (USDC and other SPL tokens) — the feed does not do per-event
RPC mint lookups, so
quoteTokenDecimals,quoteAmount,marketCapQuoteandpoolQuoteReservesUimay benull. The event then carriesquoteContextResolved: falseandquoteLookupDisabled: true.
Detect the degraded mode with quoteContextResolved === false and fall back to
the raw fields. Never assume 9 decimals for a non-SOL quote.
| Raw field | Description |
|---|---|
quoteAmountRaw | Quote amount in raw base units (buy / sell / create) |
vQuoteInBondingCurve | Raw virtual quote reserves, bonding curve |
poolQuoteReserves | Raw quote-token vault balance, PumpSwap |
virtualQuoteReserves | Pool's virtual_quote_reserves (raw base units) |
poolEffectiveQuoteReserves | poolQuoteReserves + virtualQuoteReserves — the value quotes are priced on |
create_pool is the exception: it carries normalized quote values even for
non-SOL pairs, because decimals are emitted directly in the on-chain event.
Event shapes
Bonding-curve trade (pre-migration)
No source field means the token is still on the curve.
{
"signature": "5xK9...",
"mint": "TokenMintAddress",
"traderPublicKey": "TraderWallet",
"txType": "buy",
"quoteMint": "So11111111111111111111111111111111111111112",
"quoteTokenDecimals": 9,
"quoteAmount": 0.5,
"solAmount": 0.5,
"tokenAmount": 1000000,
"bondingCurveKey": "BondingCurveAddress",
"vTokensInBondingCurve": 900000000,
"vQuoteInBondingCurve": 35000000000,
"vSolInBondingCurve": 35,
"marketCapQuote": 40.5,
"marketCapSol": 40.5
}
Pump.fun is a constant-product AMM: price is quote reserves over token reserves,
both updated after every trade. marketCapQuote is the fully diluted market cap
derived from them; vSolInBondingCurve and marketCapSol are SOL-pair aliases.
PumpSwap AMM trade (post-migration)
source: "pumpswap". This example is a USDC-quoted market, so the normalized
fields are null — the shape to expect when quoteContextResolved is false.
{
"signature": "2gT7...",
"mint": "TokenMintAddress",
"traderPublicKey": "TraderWallet",
"txType": "buy",
"quoteMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"quoteTokenDecimals": null,
"quoteAmount": null,
"tokenAmount": 2116283,
"source": "pumpswap",
"pool": "PoolAddress",
"canonicalPool": "PoolAddress",
"isCanonicalPool": true,
"baseMint": "TokenMintAddress",
"quoteAmountRaw": 24750000,
"poolBaseReserves": 800000000000,
"poolBaseReservesUi": 800000,
"poolQuoteReserves": 25000000000,
"poolQuoteReservesUi": null,
"virtualQuoteReserves": 0,
"poolEffectiveQuoteReserves": 25000000000,
"poolEffectiveQuoteReservesUi": null,
"marketCapQuote": null,
"quoteContextResolved": false,
"quoteLookupDisabled": true
}
Price PumpSwap markets on poolEffectiveQuoteReserves / poolBaseReserves — the
effective value, not the raw vault balance. Non-canonical pools for the same mint
are ignored so market-cap ticks match the pool PumpDev trades on.
New token launch (txType: "create")
{
"signature": "3hJ7...",
"mint": "NewTokenMint",
"traderPublicKey": "CreatorWallet",
"txType": "create",
"name": "Token Name",
"symbol": "TKN",
"uri": "https://metadata-uri",
"initialBuy": 100000000,
"initialQuoteAmount": 1,
"quoteMint": "So11111111111111111111111111111111111111112",
"quoteTokenDecimals": 9,
"quoteAmount": 1,
"solAmount": 1,
"bondingCurveKey": "BondingCurveAddress",
"vTokensInBondingCurve": 900000000,
"vQuoteInBondingCurve": 31000000000,
"vSolInBondingCurve": 31,
"marketCapQuote": 32.7,
"marketCapSol": 32.7,
"isMayhemMode": false,
"isCashbackEnabled": false
}
uri points at the metadata JSON (image, description). initialBuy is the
creator's opening buy in tokens, initialQuoteAmount the same in quote units
(null when the quote context is unresolved). isMayhemMode and
isCashbackEnabled report the launch options the creator chose.
Migration: complete, then create_pool
Two events fire in sequence on your existing token subscription — no
re-subscribe. Afterwards every trade for that token arrives with
source: "pumpswap".
{
"signature": "4kR2...",
"mint": "TokenMintAddress",
"traderPublicKey": "MigratorWallet",
"txType": "complete",
"bondingCurveKey": "BondingCurveAddress",
"quoteMint": "So11111111111111111111111111111111111111112",
"quoteTokenDecimals": 9,
"timestamp": 1743782400
}
{
"signature": "7jKs...",
"mint": "TokenMintAddress",
"txType": "create_pool",
"pool": "NewPoolAddress",
"traderPublicKey": "CreatorWallet",
"baseMint": "TokenMintAddress",
"pairQuoteMint": "So11111111111111111111111111111111111111112",
"quoteMint": "So11111111111111111111111111111111111111112",
"quoteTokenDecimals": 9,
"quoteAmount": 85,
"marketCapQuote": 410.83,
"source": "pumpswap",
"canonicalPool": "NewPoolAddress",
"isCanonicalPool": true,
"poolBaseReserves": 206900000000000,
"poolBaseReservesUi": 206900000,
"poolQuoteReserves": 85000000000,
"poolQuoteReservesUi": 85
}
Field reference
Shared envelope, present on every market event:
| Field | Type | Description |
|---|---|---|
signature | string | Solana transaction signature (base58) |
mint | string | Token mint address |
traderPublicKey | string | Wallet that made the trade |
txType | string | buy, sell, create, complete, create_pool |
quoteMint | string | Quote mint used by the market |
quoteTokenDecimals | number | null | Quote decimals when resolved |
quoteAmount | number | null | Trade amount in quote UI units when resolved |
marketCapQuote | number | null | Fully diluted market cap in quote units when resolved |
quoteContextResolved | boolean | Present and false when normalized fields are unavailable |
quoteLookupDisabled | boolean | Present and true when quote RPC lookups are disabled |
Bonding curve only: bondingCurveKey, vTokensInBondingCurve,
vQuoteInBondingCurve, vSolInBondingCurve (SOL alias).
PumpSwap only: source, pool, canonicalPool, isCanonicalPool, baseMint,
poolBaseReserves(Ui), poolQuoteReserves(Ui), virtualQuoteReserves,
poolEffectiveQuoteReserves(Ui).
What does the WebSocket cost?
Free to start — connect with no key at all, or create a free account for a real API key with 5× the limits. Paid tiers raise your subscription pool, monthly trade-message quota and connections.
| Tier | Price / mo | Annual | Subscriptions (live) | Launches | Conns / IP | Total conns | Trade messages / mo |
|---|---|---|---|---|---|---|---|
| Anonymous (no key) | $0 | — | 5 | Free | 1 | 1 | 10k |
| Free (API key) | $0 | — | 25 | Free | 1 | 3 | 50k |
| Starter | $49 | $39.99 | 100 | Free | 2 | 6 | 1.5M |
| Pro | $129 | $99.99 | 1,000 | Free | 5 | 15 | 6M |
| Whale | $299 | $199 | 10,000 | Free | 15 | 45 | 20M |
| Custom | from $499 | — | Unlimited | Free | Custom | Custom | 30M+ / custom |
- Only delivered buy/sell messages are metered. Launches, migrations and pool creations are always free.
- Subscriptions and connections are live counts, not monthly — they drop the moment you unsubscribe or disconnect. Only quota accumulates over the cycle.
- Anonymous means no signup, metered per IP (IPv6: per /64), resetting on the 1st (UTC). Keyed tiers reset on their renewal day, and renewing extends your existing key in place — running bots need no reconfiguration.
- Higher tiers are cheaper per trade: $32.7/M on Starter → $21.5 on Pro → $14.95 on Whale. Annual billing saves 18–33%.
Top-up packs — from the Dashboard, Adjust limits buys extra quota for the current cycle (no roll-over), requires Starter or above:
| Pack | Price (USDC) | Per million |
|---|---|---|
| +2M trades | $49 | $24.50 |
| +6M trades | $129 | $21.50 (12% off) |
| +10M trades | $199 | $19.90 (19% off) |
| +20M trades | $349 | $17.45 (29% off) |
A free key can buy one Boost pack (+500k for $15) once per account. Anonymous keys cannot top up — they soft-throttle at their quota. SOL is accepted at the spot rate; custom tiers are quoted individually.
Do I need an API key?
Not to start. To use a key, authenticate one of two ways.
Query parameter — simplest, but keys can leak into proxy and access logs:
const ws = new WebSocket('wss://pumpdev.io/ws?key=YOUR_API_KEY');
auth message — preferred in production; keeps the key out of URLs:
ws.on('open', () => {
ws.send(JSON.stringify({ method: 'auth', key: 'YOUR_API_KEY' }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'auth' && msg.status === 'ok') {
// msg.tier is your effective tier — subscribe from here.
ws.send(JSON.stringify({ method: 'subscribeTokenTrade', keys: mints }));
}
});
The server always answers with a single { "type": "auth", … } frame:
status | Meaning |
|---|---|
ok | Key applied; tier is your effective tier. |
free | Key missing, invalid, expired or revoked — you stay on the keyless tier, never disconnected. A recognised key that merely lapsed keeps the free caps; an unknown key is anonymous. |
error | Not applied; reason explains why (e.g. connection_limit). Your previous tier is kept. |
- Gate your subscriptions on the ack. Key resolution is a database lookup, so
authis answered asynchronously. Subscriptions sent meanwhile are held and applied once your tier is known, but their acks arrive after theauthframe — waiting forstatus: "ok"keeps ordering explicit and gives you one place to fail fast. - Tier is re-checked live. Expiry moves you down softly (an
authframe withstatus: "free"and areason); renewals and upgrades apply within ~30 seconds. No reconnect either way — so anauthframe can arrive unprompted and should be treated as a tier change, not only as a reply. - Auth is per connection. Every reconnect starts keyless: re-send
authinside your reconnect path.
How many subscriptions can I open?
Limits are enforced per tier (see Plans & Pricing); on the keyless feed they are also counted per IP address — on IPv6 per /64 network — across all your connections.
| Parameter | Anonymous (no key) | Free (API key) |
|---|---|---|
| Subscription pool (tokens + wallets, shared) — live | 5 | 25 |
Max mints per subscribeTokenTrade call | 20 | 50 |
Max wallets per subscribeAccountTrade call | 20 | 25 |
| Concurrent connections per IP — live | 1 | 1 |
| Monthly trade-message quota | 10k (per IP; IPv6 per /64) | 50k |
| Max control messages per 10 s | 40 | 40 |
| Max subscription key operations per 10 s | 600 | 600 |
Max keys per call is a message-size limit, not an entitlement — you may send 20 mints on the anonymous tier but hold only 5 live. An oversized batch is clamped, not rejected: what fits is subscribed, the connection stays open, and you get an error frame reporting the counts:
{
"type": "error",
"code": "SUBSCRIPTION_LIMIT",
"message": "The anonymous tier allows 5 live subscriptions (tokens + wallets) and you've reached that limit. Create a free account and get an API key at https://pumpdev.io/my-account to raise your limits.",
"accepted": 5,
"dropped": 15,
"limit": 5
}
Key operations are charged on the keys the server actually processes, not the array you sent — an oversized batch is trimmed first, so it costs a truncation notice, never a rate-limit violation. Max control messages is a flood guard that is identical on every tier: hitting it means batching keys into fewer calls, not upgrading.
A control message above 256 KB is refused before parsing with
code: "MESSAGE_TOO_LARGE" (carrying maxBytes and maxBatch). It does not
close the connection on its own; repeatedly ignoring it does.
A client that keeps reconnecting straight into a rejection — opening another
socket while its connection limit is full, or coming back to break the same
limit it was just closed for — is throttled: after repeated rejections in a
minute the handshake itself is refused with HTTP 429, a Retry-After
header, and a JSON body with code: "RECONNECT_THROTTLED" and retryAfterMs.
Repeat throttles grow the wait. A throttle caused by a full connection limit
lifts as soon as one of your sockets closes.
When you reach your trade quota, trade messages are soft-throttled until the cycle resets or you add quota — the connection stays open and launches keep flowing.
Why does the browser demo stop after a few events?
The live feed on our homepage is a demo socket, scoped differently from the
Anonymous tier: subscribeNewToken only (trade subscriptions return
code: "DEMO_SCOPE"), 5 per IP, not counted against your keyless allowance, and
closed after 60 seconds with a notice / DEMO_SESSION_END frame. Only sockets
opened from a browser tab on our own site are affected — your own client, keyless
or keyed, is not.
Production best practices
- Reconnect with exponential backoff and re-subscribe (and re-
auth) on open — see the snippet below. On an HTTP 429 handshake, wait for itsRetry-Afterbefore trying again. - Unsubscribe before closing so server-side resources are released.
- Wrap
JSON.parsein try/catch so one malformed frame cannot crash your handler. - Watch for silence. The feed is continuous; no messages within your expected interval usually means a dropped connection.
- Batch keys into one call rather than one call per mint — but keep the batch within your subscription pool, since anything over it is clamped away.
function connectWithRetry(url, delay = 1000) {
const ws = new WebSocket(url);
let wait = delay;
ws.on('open', () => { /* re-auth + re-subscribe here */ });
// Throttled handshake: the server says how long to wait.
ws.on('unexpected-response', (_req, res) => {
const retryAfter = Number(res.headers['retry-after']);
if (retryAfter > 0) wait = Math.max(wait, retryAfter * 1000);
ws.terminate(); // emits 'close', which schedules the retry
});
ws.on('error', () => {});
ws.on('close', () => {
setTimeout(() => connectWithRetry(url, Math.min(delay * 2, 30000)), wait);
});
return ws;
}
Common use cases
| Use case | Method | Notes |
|---|---|---|
| Sniper bot (new launches) | subscribeNewToken | React to txType: "create" |
| Token price tracking | subscribeTokenTrade | vQuoteInBondingCurve / vTokensInBondingCurve on the curve; poolEffectiveQuoteReserves / poolBaseReserves on PumpSwap |
| Whale wallet monitoring | subscribeAccountTrade | Pass known whale addresses |
| Market cap alerts | subscribeTokenTrade | Threshold on marketCapQuote; require quoteContextResolved !== false |
| Copy trading | subscribeAccountTrade | Mirror trades from target wallets |
| Migration detection | subscribeTokenTrade | React to complete and create_pool |
| PumpSwap pool tracking | subscribeTokenTrade | Filter source === "pumpswap" (canonical pool only) |
Pair the feed with Lightning Trade to act on an event in a single call.
Frequently asked questions
Do I need an API key to stream pump.fun data?
No. The feed accepts keyless connections and streams immediately, metered per IP address (on IPv6, per /64 network) with 5 subscriptions and 10k trade messages a month. A free account takes about half a minute and raises that to 25 subscriptions and 50k messages on a key of your own, which also makes usage visible in the dashboard.
Are new token launches counted against my quota?
No. subscribeNewToken is free on every tier including keyless, and launches,
migrations and pool-creation events never touch the trade quota. Only delivered
buy and sell messages are metered.
What happens when a token migrates from the bonding curve to PumpSwap?
Two events fire in sequence on the same subscription: txType: "complete" when
the curve fills, then txType: "create_pool" when the canonical AMM pool opens.
You do not re-subscribe — post-migration trades keep arriving with
source: "pumpswap".
Why is quoteAmount sometimes null?
Because the pair is quoted in something other than SOL and the quote context was
not resolved. Check quoteContextResolved: when it is false, read the raw
fields instead — quoteAmountRaw, vQuoteInBondingCurve,
poolEffectiveQuoteReserves. Never assume 9 decimals for a non-SOL quote.
What happens if I run out of trade quota?
Your connection stays open and launches keep flowing — only trade messages are soft-throttled until the cycle resets. Paid tiers can buy top-up packs from the Dashboard at a lower per-million price than the base plan; a free key gets one Boost pack, and keyless connections cannot top up at all.
Do I have to re-authenticate after reconnecting?
Yes. Auth is per connection, so every reconnect starts keyless — send the auth
message again inside your reconnect path and wait for status: "ok" before
subscribing. An auth frame can also arrive unprompted mid-connection when your
tier changes.
Is there a feed for chains other than Solana?
Yes — the same account and key stream PONS on Robinhood Chain over the PONS WebSocket, metered on a separate counter that never spends your Solana quota.
Trading is there too: the PONS Trading API is the same
/api/trade-local shape, on rhc.pumpdev.io.
Next steps
- Dashboard — get an API key, pick a plan, track live usage
- ⚡ Lightning Trade — one-call buy/sell, built for sniping
- Trade API — client-side buy/sell transactions
- Token Creation — create new tokens on Pump.fun
- Fees — trading fees (separate from the WebSocket tiers above)
Join the PumpDev Telegram community for support and examples from other developers building on Pump.fun.