PONS WebSocket API: Real-Time Robinhood Chain Data
One socket streams every PONS event on Robinhood Chain — new token launches, curve trades and graduations from the ponsfamily.com launchpad — delivered as transactions enter the sequencer, before a block confirms.
Endpoint: wss://rhc.pumpdev.io/ws
Authentication: ?key=<API_KEY> — the same key as the Solana feed. One plan covers both chains.
Free without a key: launches, graduations and metadata. Trades require a key.
The PONS Trading API turns a newToken event into a signed
buy — one POST returns an unsigned transaction with the route and the slippage
floor already solved. No API key needed for it.
Robinhood Chain is an Arbitrum Orbit L2 (chain ID 4663) with no public mempool and no priority-fee auction: the sequencer orders by arrival time, so latency is the only edge. Explorer: rh-scan.com.
Quick start
import WebSocket from 'ws';
const ws = new WebSocket('wss://rhc.pumpdev.io/ws?key=YOUR_KEY');
ws.on('open', () => {
ws.send(JSON.stringify({ method: 'subscribeNewToken' }));
ws.send(JSON.stringify({
method: 'subscribeTokenTrade',
keys: ['0xTokenAddress']
}));
});
ws.on('message', (data) => {
const event = JSON.parse(data.toString());
console.log(event.type, event.status, event.token);
});
Subscriptions
| Method | Payload | Delivers | Key |
|---|---|---|---|
subscribeNewToken | — | newToken, graduation, tokenMeta | no |
subscribeTokenTrade | keys: [tokenAddress] | tokenTrade for those tokens | yes |
subscribeAccountTrade | keys: [walletAddress] | tokenTrade by those wallets | yes |
Each has a matching unsubscribe… method with the same payload. Every call is
acknowledged:
{ "type": "subscribed", "method": "subscribeTokenTrade", "keys": ["0x…"] }
keys lists what was actually accepted — invalid or over-cap keys are trimmed,
not refused (see SUBSCRIBE_NORMALIZED).
Event statuses: deduplicate by txHash
Every event says where it came from, and most events arrive twice.
status | Source | Meaning |
|---|---|---|
pending | sequencer calldata | in flight, may revert — this is the head start |
confirmed | executed block logs | settled fact |
Collapse pairs by txHash. A launch arrives pending then confirmed; a trade
arrives pending then confirmed ~500ms later. A pending launch has
token: null — the CREATE2 address only exists after execution — so txHash is
the only correlation key.
The two stages read different sources, and together they cover the whole
market: pending decodes PONS router calldata for the head start, while
confirmed reads every block on the chain — aggregators, the Universal Router
and post-graduation pools included. Graduations are confirmed only.
Porting a pump.fun bot
The wire contract matches the Solana feed frame for frame — same methods, same handshake, same acks. Change the URL and, usually, nothing else. Every event carries both field names:
| pump.fun | PONS |
|---|---|
txType: "create" | type: "newToken" |
txType: "buy" | "sell" | type: "tokenTrade", side |
signature | txHash (an EVM transaction hash) |
mint | token |
traderPublicKey | trader |
Both names are present in every event. The PONS names are authoritative and are what the shapes below use; the pump.fun names are aliases kept so a working bot runs unchanged.
- Events arrive twice (
pending+confirmed). A bot countingcreateevents will double count — filter onstatusor collapse bytxHash. - Amounts are in the quote asset, not a fixed 18-decimal native token. Read
quote.decimalsbefore formatting anything.
Event shapes
newToken — pending
{
"type": "newToken",
"txType": "create",
"status": "pending",
"token": null,
"deployer": "0x…",
"name": "StockPacks",
"symbol": "PACKS",
"imageUri": "ipfs://…",
"description": "…",
"socials": ["https://x.com/…", "", "", "", ""],
"creator": "0x…",
"pairToken": "0x…",
"quote": { "symbol": "USDG", "decimals": 6 },
"devBuyWei": "8800000000000000",
"snipeTaxExempt": ["0x…", "0x…"],
"salt": "0x…",
"block": 1234567,
"blockTimestamp": 1789466847,
"txHash": "0x…"
}
blockTimestamp— theblock.timestampof the block this launch executes in, straight from the sequencer feed. The curve's snipe tax steps down per whole second from this value (9900 → 618 → 19 → 0 bps at +0/+1/+2/+3 s), soblockTimestamp + 3is the first second a stranger buys untaxed. See The snipe tax.nullwhen the feed did not carry it.snipeTaxExempt— the wallets the creator named at launch to buy the first second tax-free: the bundle team.[]means nobody beyond the creator;nullmeans the launch call is one we cannot read this from, not an empty list. Eight addresses here is eight wallets buying at 0% while you would pay 99%.devBuyWei— the launcher's own opening buy, in quote-asset units. This is the only place a dev buy is visible (initialBuyWeion the confirmed log reports0)."0"means no buy;nullmeans unknown, not zero.deployer— whoever called the factory: the wallet, or the launch wrapper it went through.creatoris the fee recipient, not always the same.salt— the CREATE2 salt; simulating the transaction returns the token address at any block.- Metadata (
name,symbol,imageUri, …) comes from calldata, so it arrives beforetokenMeta. Every key is always present: an unusual launch wrapper still delivers the event on time, with the fields it did not carry asnull.
newToken — confirmed
{
"type": "newToken",
"txType": "create",
"status": "confirmed",
"token": "0x…",
"deployer": "0x…",
"pairToken": "0x…",
"quote": { "symbol": "USDG", "decimals": 6 },
"pool": "0x…",
"initialBuyWei": "1000000000000000",
"graduationThreshold": "42000000000",
"block": 1234567,
"blockTimestamp": 1789466847,
"txHash": "0x…"
}
When the launch was also seen pending, the confirmed event repeats its metadata
fields, snipeTaxExempt included. Log-derived fields (token, pairToken, pool,
graduationThreshold) always win over calldata.
pool is the token's bonding curve — the contract every router swap names
as its venue until graduation, and the one the
trading API prices from. Its buys are taxed for the first
3 seconds after launch — 99% in the launch second, 6.18% the next, 0.19% the
one after, then 0 — so a bot that acts on this event should buy from
blockTimestamp + 3, or let the trade response's snipeTaxBps say what the
curve would take. See The snipe tax.
initialBuyWei and graduationThreshold are in quote-asset units — assuming
18 decimals on a USDG pair is off by 10¹².
tokenTrade — pending
{
"type": "tokenTrade",
"txType": "buy",
"status": "pending",
"token": "0x…",
"trader": "0x…",
"side": "buy",
"amountIn": "1000000",
"amountOutMin": "980000000000000000",
"quote": { "symbol": "USDG", "decimals": 6 },
"hops": 2,
"ponsVenue": true,
"txHash": "0x…"
}
amountIn is what the trader spends, amountOutMin the slippage floor. Neither
is an executed amount.
tokenTrade — confirmed
{
"type": "tokenTrade",
"txType": "buy",
"status": "confirmed",
"token": "0x…",
"trader": "0x…",
"side": "buy",
"amountToken": "41053416785771512631551",
"amountQuote": "1608718913688474",
"amountIn": "1608718913688474",
"amountOut": "41053416785771512631551",
"quote": { "symbol": "USDG", "decimals": 6, "assetClass": "erc20", "address": "0x…" },
"amountQuoteEth": "31929000000000000",
"ethRate": { "ethPerUnit": "0.000404412321388955", "source": "implied", "ageBlocks": 0, "observedBlock": 57700908 },
"venue": "0x…",
"block": 1234567,
"txHash": "0x…"
}
amountTokenis always the token;amountQuotethe other side of the swap.amountIn/amountOutstate the same trade from the trader's point of view, soamountInreads the same across both statuses.amountQuoteEth—amountQuoteconverted to wei of ETH so tokens on different pairs are comparable. Native units are never rewritten. The rate is an observation median from the same blocks, not an oracle;sourceisnativeorimplied. When no recent rate exists,amountQuoteEthandethRatearenull, never stale.venue— the pool at the end of the route. Reported, never filtered on.trader— the wallet or contract receiving or giving up the token.nullwhen there is no identifiable one (an arbitrage loop, a pool-to-pool hop). Naming a pool would be worse than saying nothing.amountQuote: nullmeans unknown, never zero — the trade is delivered either way.- Several trades can share one
txHash(~1% of transactions — bundler batches), published largest first. Collapsing bytxHashkeeps the largest; collapse by(txHash, trader)to keep them all. A payout split across addresses is one trade, not several. - Adding or removing liquidity is not published as a trade.
graduation
{ "type": "graduation", "status": "confirmed", "token": "0x…", "block": 1234567, "txHash": "0x…" }
tokenMeta
{ "type": "tokenMeta", "token": "0x…", "name": "Example", "symbol": "EXMPL", "decimals": 18 }
Fetched in a batch just after the launch so the launch never waits on a round
trip. Any field can be null.
Quotas and keys
| Event | Key required | Metered |
|---|---|---|
newToken | no | no |
graduation | no | no |
tokenMeta | no | no |
tokenTrade | yes | yes |
One plan, both chains. PONS trades are recorded on their own meter but draw on the same monthly allowance as pump.fun trades; connection and subscription caps are shared too. Both metered statuses count, so a busy token spends quota faster than its number of distinct trades — budget for frames, not for trades.
Create a key at My Account; tiers and prices are on the pricing page.
Errors and control frames
Everything arrives as a normal message on the same socket:
{ "type": "error", "code": "CONNECTION_LIMIT", "message": "This key allows 2 simultaneous connections on the starter plan." }
Switch on code, show message. (error duplicates message for older
clients; new code should ignore it.)
code | When | Socket |
|---|---|---|
DEMO_LIMIT | too many landing-page demo sockets from one IP | closed, 1013 |
CONNECTION_LIMIT | over your plan's per-key or per-IP cap | closed, 1013 |
SERVER_AT_CAPACITY | the service is full | closed, 1013 |
INVALID_JSON | message is not JSON | stays open |
UNKNOWN_METHOD | unknown method | stays open |
Cap refusals are queued before the close and carry upgradeUrl
(CONNECTION_LIMIT also carries tier) — read the frame, not the close reason.
A close with 1013 / slow consumer means your client stopped reading fast
enough.
Non-error control frames:
connected+connectionStatus— sent in that order on connect;connectionStatusrepeats whenever the upstream feed drops or returns, so you can tell a quiet chain from a dead feed.notice/SUBSCRIBE_NORMALIZED— partial success, not a rejection: valid keys are subscribed and the ack follows. Retrying the batch will not help.invalidcounts keys this feed cannot parse andtruncatedcounts a batch clipped by your plan's batch size — a tier never shows up asinvalid, sotruncated: 0rules your plan out. When the keys are well-formed but belong to the Solana feed,reasonisWRONG_CHAIN_ENDPOINTanduseEndpointnames the URL to use (wss://pumpdev.io/ws);invalidKeyslists what was dropped.quota/exceeded— monthly trade allowance spent. Trades stop; launches, graduations and metadata keep flowing, socket stays open.auth/free|ok— your key's tier changed (expired, revoked, renewed, upgraded), at any time or on connect. Onfreeyou are moved down, not disconnected;trimmedSubscriptionssays how many were dropped.notice/NO_API_KEY— connected with no key at all. Launches still stream, metered by IP.
Good to know
pendingis intent,confirmedis settlement. A pending trade is decoded before execution, so settle on the confirmed event and use the pending one for the head start.- Confirmed trades stream from the moment you subscribe, including a trade in the same block as the subscription itself.
- Any PONS token works, at any age. Unknown tokens are verified against the
PONS factory (
getLaunchedToken, via Multicall3) before trades flow, typically within a fraction of a second, and RPC hiccups are retried rather than written off — graduated tokens included.
Frequently asked questions
Do I need an API key to stream PONS launches?
No. subscribeNewToken works with no key and is not metered — launches,
graduations and token metadata are free on every plan. A key is required only
for subscribeTokenTrade and subscribeAccountTrade.
Does streaming PONS use up my pump.fun quota?
Yes, for trade events. One plan covers both chains and they share a single monthly trade allowance. Launches, graduations and metadata never count.
Can I subscribe to a PONS token that launched weeks ago?
Yes. Unknown tokens are verified against the PONS factory on chain, so age does not matter — graduated tokens included.
What is the difference between a pending and a confirmed trade?
A pending trade is decoded from the sequencer before execution — that is the head
start. The confirmed event, from the executed block, is the settled fact and
carries the executed amounts. Act early on pending, settle on confirmed.
What quote assets do PONS launches use?
Several, with different decimals: many pair against USDG (6 decimals), others
against ETH, WETH or tokenized equities and ETFs (NVDA, TSLA, QQQ, GLD and
dozens more, all 18-decimal). Always read the quote object; pairToken
always gives the address of the asset a token is paired against.
Is there a REST API for trading on PONS?
Yes. POST https://rhc.pumpdev.io/api/trade-local builds an unsigned EVM
transaction you sign locally and broadcast yourself — the route, the gas and the
slippage floor are solved for you, and no API key is required. See the
PONS Trading API. Token creation is still on the roadmap.
Related
- PONS Trading API — buy and sell the tokens this feed reports
- PONS Family API — what the feed is and who it is for
- Solana WebSocket API — the same contract for pump.fun
- Getting Started — accounts, keys and the rest of the API