Skip to main content

PONS Trading API: Build a Robinhood Chain Trading Bot in One POST

Buy and sell PONS tokens from ponsfamily.com on Robinhood Chain programmatically, with full wallet custody. Your private key never leaves your machine: PumpDev returns an unsigned transaction, you sign it locally and broadcast it yourself.

Prefer one call, no keys on your side?

PONS Lightning signs and sends for you β€” same body without publicKey, 1% commission.

One request gives you a transaction that is ready to send β€” the route solved, the gas measured, the slippage floor computed, the quote asset and its decimals already handled. No API key. No signup. No custody.

API Endpoint: POST https://rhc.pumpdev.io/api/trade-local

Pair it with the PONS WebSocket API to react to launches as they happen β€” the feed tells you a token exists, this endpoint lets you trade it in the same breath.


Why Use the PumpDev PONS Trading API​

Robinhood Chain produces a block every ~100ms and orders transactions by arrival, not by fee. There is no priority auction to win β€” which means the only edge is how fast you can put a valid transaction on the wire. This API exists to make that part disappear.

  • Built in about 200ms. Measured end to end against the live chain, on real buys and sells. The route and the price come from a feed that decodes every swap on the chain as it happens, so the common path never waits on a lookup.
  • Non-custodial by construction. PumpDev only ever sees your public key. There is no key to leak, because there is no key.
  • Routes you cannot compute yourself. PONS routes are multi-hop, and a pre-graduation curve has no pool address anywhere in its launch log. PumpDev knows the route because it watches the chain, not because it guesses.
  • Every quote asset handled. PONS tokens launch against ETH, USDG and tokenized equities. The pair and its decimals are read from the route, so amount means what you think it means on every token.
  • A floor you can trust. Every quote comes back with a real amountOutMin computed from the last executed trades β€” and the API refuses to build a trade it cannot protect, rather than handing you a signature with nothing behind it.
  • Your nonce, your gas. Override either. On a first-come-first-served chain a bot firing several trades in a row keeps its own nonce, and this API expects that instead of fighting it.

Prerequisites​

  • Node.js 18+ with ethers v6
  • A Robinhood Chain wallet with ETH for gas
  • Nothing else β€” no API key

Request Parameters​

ParameterTypeRequiredDescription
publicKeystringYesYour wallet address. Used as from, and to read your nonce, balance and allowance
actionstringYes"buy" or "sell"
tokenstringYesThe PONS token address. mint is accepted as an alias, so a bot written for pump.fun works by changing the URL
amountnumber/stringYesAmount to trade β€” see below
denominatedInQuotestringNoSet "true" when a buy amount is in the pair's quote asset: ETH for ETH pairs, USDG for USDG pairs. Keep "false" for sell percentages and exact token amounts. denominatedInSol is accepted as an alias
slippagenumberNoSlippage percentage (default: 90)
priorityFeenumberNoGwei per gas, becomes maxPriorityFeePerGas (default: 0.05). Not ETH β€” see the warning below
gasLimitstringNoOverride the gas limit. Skips estimation entirely
noncenumberNoOverride the nonce. Skips the lookup entirely
deadlinenumberNoSeconds until the swap expires on-chain (default: 60)
amountOutMinstringNoSet the minimum output yourself, in base units. Overrides both the quoted price and slippage
maxSnipeTaxBpsnumberNoThe highest PONS snipe tax, in basis points, you will pay on a buy (default: 0 β€” any tax is refused). 700 accepts the 6.18% second-second step, 20 only the 0.19% one. See The snipe tax
priorityFee is in gwei, not ETH

This is the one parameter that carries its name over from the Solana API with a different unit, and the mistake is expensive. EVM prices gas per unit of gas, so priorityFee: 0.001 means 0.001 gwei β€” but if you pass what you would pass on Solana thinking in ETH, you are asking to pay a million gwei on roughly 450,000 gas, which is hundreds of ETH in tip. It is paid on inclusion and cannot be recovered.

Anything above 100 gwei is refused outright rather than built. For reference, measured on Robinhood Chain: base fee ~0.2 gwei, real tips ~0.02 gwei at the median and 1.5 gwei at the very top. The chain orders transactions by arrival rather than by fee, so a bigger tip does not get you in sooner.

Amount Formats​

For buy transactions:

  • 0.05 with denominatedInQuote: "true" = buy with 0.05 ETH
  • 25 with denominatedInQuote: "true" on a USDG-quoted token = buy with 25 USDG

For sell transactions:

  • "100%" = sell your whole balance
  • "50%" = sell half
  • 1000000 with denominatedInQuote: "false" = sell an exact token amount

Response​

{
"transaction": {
"chainId": 4663,
"type": 2,
"from": "0xYourWallet",
"to": "0x65050A9b7E5075A2bA5cED7b1b64EE66262c40Dc",
"data": "0x4d819a2a...",
"value": "50000000000000000",
"gasLimit": "312500",
"maxFeePerGas": "2500000",
"maxPriorityFeePerGas": "1000000000000000",
"nonce": 41
},
"approvalTransaction": null,
"gasEstimated": true,
"quote": {
"quoteAsset": { "address": "0x0000000000000000000000000000000000000000", "symbol": "ETH", "decimals": 18 },
"amountIn": "50000000000000000",
"expectedOut": "1000000000000000000000",
"amountOutMin": "900000000000000000000",
"priceSource": "curve",
"priceAgeMs": 0,
"snipeTaxBps": 0,
"snipeTaxEndsAt": null,
"routeSource": "registry"
},
"route": [
{ "kind": 27, "tokenIn": "0x0000...", "tokenOut": "0xToken", "pool": "0xPool", "fee": 0 }
],
"fee": { "bps": 0, "amount": "0", "asset": "ETH" }
}

Every amount is a decimal string in base units. A uint256 does not survive a JSON number, so nothing here is one.

transaction is shaped for wallet.sendTransaction(...) in ethers and viem β€” pass it straight through.

Reading quote​

amountIn and expectedOut are both wallet-facing: what leaves your wallet, and what lands in it after commission. On a buy that means amountIn is the gross you spend while the swap itself sees slightly less; on a sell it means expectedOut is already net of our cut. amountOutMin is a floor on the same wallet-facing figure, so it means one thing in both directions: the least you will accept to end up with.

priceSource tells you where expectedOut came from:

ValueMeaning
curveComputed from the bonding curve's own reserves at build time, for a token still on its curve. Exact, priceAgeMs is 0, and it works from the launch block β€” no trade needs to have happened
poolComputed from the Uniswap v4 pool a graduated token trades in, at build time, with the pool's, the PONS hook's and the PONS router's fees included. Exact, priceAgeMs is 0, and it does not need anyone to have traded the token recently
feedThe token's last executed trade, as the live feed saw it, priceAgeMs old
logsThe token's last executed trade, read back off the router's own logs. The same number from a colder source, and usually a little older
callerYou passed amountOutMin yourself; PumpDev did not price it

A curve or pool price is a quote against current liquidity, price impact included: it is what the curve or pool will pay for this amount if nothing trades ahead of you. feed and logs are what a graduated token falls back to when its pool cannot be read. feed and logs both mean the last trade that actually executed, and neither accounts for your own price impact. Read priceAgeMs, not priceSource, to judge how much to trust a historical one.

A logs price is the median of the last few trades of that token, not the newest one. The amounts in a trade are chosen by whoever sent it, so a single swap through a pool its own sender controls can name any ratio it likes; a median needs such trades to outnumber the real ones. Where only one recent trade exists β€” a token minutes into its life β€” that is what you get, and it is worth a wider slippage or an amountOutMin of your own.

The snipe tax​

Every PONS curve taxes buys in the first seconds after launch, and the schedule is neither linear nor per block. It steps down once per whole second of block.timestamp since the launch block (measured per block on the live chain, 2026-09-15):

Seconds since the launch blockTax on the buy
09900 bps (99%)
1618 bps (6.18%)
219 bps (0.19%)
3+0

The 99% step therefore lasts anywhere from one block to ten β€” it ends at the next timestamp tick, not ten blocks after launch. Sells are never taxed, and the creator's own wallets (the launcher, the fee recipient, and every address in the launch's snipeTaxExempt list on the newToken event) read 0 throughout.

The API reads the rate for your wallet from the curve at build time and reports it as quote.snipeTaxBps, with quote.snipeTaxEndsAt β€” the Unix second at which it reaches 0 β€” while it is non-zero. What happens next depends on maxSnipeTaxBps, the ceiling you set on the request:

  • Tax at or below your ceiling β€” the buy is built. The tax is already inside expectedOut and amountOutMin; you are paying a cost you consented to, and the quote tells you the rate.
  • Tax above your ceiling, or no ceiling sent β€” HTTP 425 with snipeTaxBps, launchedAt, snipeTaxSeconds, snipeTaxEndsAt and retryAfterMs. Sleep retryAfterMs, then build again; nothing needs polling.
  • You passed amountOutMin β€” the curve is not consulted and the buy is built whatever the tax. That floor is your protection.

maxSnipeTaxBps is deliberately separate from slippage: slippage is your tolerance for the price moving between quote and execution, the ceiling is consent to a cost that is known exactly at build time. It is the same knob a tax-trigger sniper exposes β€” "fire once the launch tax is under X%" β€” with the waiting left to you, because you know when the next launch is worth more than this one. maxSnipeTaxBps: 700 takes the second second (6.18%), 20 only the third (0.19%), and the default 0 waits for a clean curve.

The ceiling costs nothing: the tax and its schedule are read in the same Multicall as the curve's reserves, so a build with maxSnipeTaxBps makes no request a build without it would not. Between the build and the block your transaction lands in, the tax can only fall, so a quote is never optimistic about it.

Where the route comes from​

For a token still on its bonding curve the route is derived from the launch itself: the launch log names the curve, and a curve hop has no other degrees of freedom, so the token is tradeable from the block it launched in. Every other route is one PumpDev observed being used β€” from the feed, or read back off the PONS router's own logs β€” replayed into your transaction as it was seen, hop for hop, because the router accepts nothing else: a route has to be a path that already worked.

That means the route is only as trustworthy as the trade it was copied from, and a trade is sent by whoever sent it. There is no allowlist of pools behind this β€” measured on live traffic, PONS tokens trade across Uniswap v3, v4 and a third venue, and a list strict enough to be meaningful would refuse most real trades.

amountOutMin is what actually protects you, and it protects you against this too: a route that does not deliver reverts the transaction rather than paying out. That is why we refuse to build a trade without a floor, and why a token with only one recent trade behind its price is worth a floor you set yourself. The cost of a bad route is gas, not the trade.

A quote never comes back with amountOutMin of 0. If the recent trades price a token too thinly to protect the amount you asked for, the request is refused instead. A zero floor authorises the entire amount to be taken, and that is a decision only you can make β€” pass amountOutMin: "0" yourself if you mean it.

priceAgeMs matters on a fresh launch. Robinhood Chain produces a block every ~100ms, and a price a few seconds old on a token minutes into its life can be far from the current one. With a tight slippage that means a reverted transaction and spent gas. Either keep slippage high, or compute amountOutMin yourself and pass it.

There is no simulation option. The router’s swap() returns no data, so an eth_call against it comes back empty and cannot tell you the output.


How to Buy PONS Tokens​

import { JsonRpcProvider, Wallet } from 'ethers';

const API_URL = 'https://rhc.pumpdev.io';
const RPC_URL = 'https://rpc.mainnet.chain.robinhood.com'; // public Robinhood Chain RPC
const PRIVATE_KEY = '0xYourPrivateKey';

const provider = new JsonRpcProvider(RPC_URL, 4663, { staticNetwork: true });
const wallet = new Wallet(PRIVATE_KEY, provider);

async function buyToken(token) {
const response = await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey: wallet.address,
action: 'buy',
token,
amount: 0.05, // 0.05 of the pair's quote asset
denominatedInQuote: 'true',
slippage: 15
})
});

if (!response.ok) {
console.error('API error:', await response.json());
return;
}

const { transaction, quote } = await response.json();
console.log('Expected out:', quote.expectedOut, `(price ${quote.priceAgeMs}ms old)`);

const sent = await wallet.sendTransaction(transaction);
console.log('Sent:', sent.hash);

const receipt = await sent.wait();
console.log(receipt.status === 1 ? 'Confirmed' : 'Reverted');
}

How to Sell PONS Tokens​

A sell moves an ERC-20, so the contract that executes it must be allowed to pull the token. When your allowance is short, the response carries an approvalTransaction β€” send it first, wait for it, then send the trade.

It comes back on the same /api/trade-local response as the trade itself; there is no separate endpoint to call for it. It is a bare approve(spender, max) on the token, so it carries only the fields that call needs β€” your wallet fills in from, nonce, gas limit and fees the way it does for any transaction:

"approvalTransaction": {
"chainId": 4663,
"type": 2,
"to": "0xTokenAddress",
"data": "0x095ea7b3...",
"value": "0"
}

While that approval is still outstanding the trade cannot be measured: eth_estimateGas executes the call, and the swap's transferFrom would revert for the very reason you are about to fix. So the response comes back with gasEstimated: false and a safe fixed gasLimit. Build again after the approval confirms and you get a measured one, which is what the example below does.

async function sellToken(token) {
const response = await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey: wallet.address,
action: 'sell',
token,
amount: '100%',
slippage: 15
})
});

let built = await response.json();

// One-time per token and spender. Nothing to send when it is null.
if (built.approvalTransaction) {
const approval = await wallet.sendTransaction(built.approvalTransaction);
await approval.wait();
console.log('Approved:', approval.hash);

// Build again now that the allowance exists, so the gas limit is measured
// rather than the fallback.
built = await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey: wallet.address,
action: 'sell',
token,
amount: '100%',
slippage: 15
})
}).then((r) => r.json());
}

const sent = await wallet.sendTransaction(built.transaction);
await sent.wait();
console.log('Sold:', sent.hash);
}

Executing Partial Sells​

Sell a percentage of your position, or an exact number of tokens, instead of closing it entirely. A percentage is applied to the balance the chain reports at build time, so "100%" is the balance itself rather than a figure you have to keep in sync.

// Sell half the position
await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey,
action: 'sell',
token: 'PonsTokenAddress',
amount: '50%',
slippage: 15
})
});

// Or an exact token amount, in whole tokens
await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey,
action: 'sell',
token: 'PonsTokenAddress',
amount: '1000000',
slippage: 15
})
});

An exact amount larger than your balance is refused while it is still free β€” before the transaction is built, and long before it could cost gas on chain.


Building an Automated PONS Trading Bot​

Robinhood Chain is first-come-first-served with no mempool, so a bot that sends two trades back to back must number them itself. Read the nonce once and pass it on every request:

let nonce = await provider.getTransactionCount(wallet.address, 'pending');

for (const token of tokens) {
const response = await fetch(`${API_URL}/api/trade-local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
publicKey: wallet.address,
action: 'buy',
token,
amount: 0.01,
denominatedInQuote: 'true',
nonce: nonce++
})
});
const { transaction } = await response.json();
wallet.sendTransaction(transaction).catch((e) => console.error(e.message));
}

Without nonce, every request asks the node for the pending count and they all get the same number β€” only one of the trades lands.


Quote Without Building​

GET /api/quote runs the same route lookup and the same maths and skips gas estimation, so it answers for a wallet that holds nothing yet.

GET https://rhc.pumpdev.io/api/quote?publicKey=0x...&action=buy&token=0x...&amount=0.05&denominatedInQuote=true

Differences from the Solana Trading API​

If you already use /api/trade-local for pump.fun, the shape is familiar. What changed, and why:

SolanaRobinhood ChainWhy
minttokenEVM tokens are contracts, not mints
denominatedInSoldenominatedInQuoteThe quote asset is often not ETH
Binary response bodyJSONEVM has no canonical serialized unsigned transaction; sendTransaction takes fields
priorityFee in SOLpriorityFee in gwei per gasEVM prices gas per unit, not per transaction
No approvalsapprovalTransactionERC-20 needs an allowance before anything can pull it
Blockhash expirydeadline + nonceOrdering and expiry work differently on an EVM chain

Error Handling & Troubleshooting​

ErrorRoot CauseSolution
Missing required fields: publicKey, action, token, amountA field is absentSend all four
action must be 'buy' or 'sell'Invalid actionUse exactly "buy" or "sell"
Invalid publicKey or token addressNot an EVM addressCheck the address format
No known route for this tokenNot a PONS token, or a graduated one whose pool PumpDev has not seen tradeVerify the token address. A PONS token still on its bonding curve is routed at any age, from the token contract itself; a graduated token launched before PumpDev's window needs one trade to be observed
No token balance foundSelling more than you hold, or nothing at allCheck your balance
Invalid percentage value β€” use 1% to 100%A percentage outside the rangeUse "50%" or "100%"
Calculated sell amount is zeroThe percentage rounds to nothingSell a larger share
Price unavailable β€” pass amountOutMin explicitlyA graduated token whose pool could not be read and that has not traded recently enough to price against, or whose recent trades were in a different asset than the route pays in (e.g. priced in USDG, routed through ETH; ETH and WETH count as the same asset)Pass amountOutMin yourself
Snipe tax of N bps applies to buys this soon after launch… (HTTP 425)The token launched under 3 seconds ago and the curve's tax on your buy exceeds your maxSnipeTaxBps (default 0)Sleep retryAfterMs and build again, raise maxSnipeTaxBps to accept the tax, or pass amountOutMin to buy anyway. See The snipe tax
maxSnipeTaxBps must be an integer from 0 to 10000A fraction, a negative number or something that is not a numberSend whole basis points: 700 for 7%
Token has graduated and its pool route is not known yetThe curve is done and the pool route has not been observedRetry in a moment
Refusing to build a trade with no floor…The recent trades price the token too thinly to protect this amountTrade a larger amount, or pass amountOutMin yourself
Gas estimation failed: ...The trade would revert on-chainRead the revert reason β€” usually slippage, allowance or a stale route

Frequently Asked Questions​

Do I need an API key?​

No. There is no key, no signup and no rate-limit tier to pick. The commission on each trade is what pays for the service, exactly as on the pump.fun Trading API.

Do I have to share my private key?​

No. PumpDev only ever sees your wallet address. You get back an unsigned transaction, sign it locally with your own key, and broadcast it yourself.

Can I point an existing pump.fun bot at this?​

Largely, yes β€” that is the intent. mint and denominatedInSol are accepted as aliases for token and denominatedInQuote, the actions and the amount formats are the same, and the response is JSON instead of a serialized Solana transaction. The one thing to change deliberately is priorityFee, which is gwei per gas here rather than a total in the native asset.

What slippage should I use?​

For a token minutes into its life, 15-25% is realistic on any chain. On Robinhood Chain blocks arrive about every 100ms, so a price a few seconds old can have moved β€” read priceAgeMs in the response and widen the tolerance when it is high, or set amountOutMin yourself.

Which quote assets are supported?​

All of them. PONS tokens launch against ETH, USDG and tokenized equities, and the pair and its decimals are read from the route, so amount means the same thing on every token. A buy in an ERC-20 quote returns an approvalTransaction to send first; a native ETH buy needs no approval at all.

How fast is a build?​

Measured end to end against the live chain, a build lands in roughly 200ms once the route is known, and the API keeps the routes and prices for every PONS token warm from a feed that decodes the chain as it happens. The first request for a token that has not traded in a while is the slow one.

Why is my first request slower than the rest?​

The first HTTPS request from a fresh process pays DNS, TCP and TLS handshake costs before the API sees it at all. Warm the connection at startup with a GET /api/health, and keep it alive while idle so trades reuse it.


Next Steps​