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.
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
amountmeans what you think it means on every token. - A floor you can trust. Every quote comes back with a real
amountOutMincomputed 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
ethersv6 - A Robinhood Chain wallet with ETH for gas
- Nothing else β no API key
Request Parametersβ
| Parameter | Type | Required | Description |
|---|---|---|---|
publicKey | string | Yes | Your wallet address. Used as from, and to read your nonce, balance and allowance |
action | string | Yes | "buy" or "sell" |
token | string | Yes | The PONS token address. mint is accepted as an alias, so a bot written for pump.fun works by changing the URL |
amount | number/string | Yes | Amount to trade β see below |
denominatedInQuote | string | No | Set "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 |
slippage | number | No | Slippage percentage (default: 90) |
priorityFee | number | No | Gwei per gas, becomes maxPriorityFeePerGas (default: 0.05). Not ETH β see the warning below |
gasLimit | string | No | Override the gas limit. Skips estimation entirely |
nonce | number | No | Override the nonce. Skips the lookup entirely |
deadline | number | No | Seconds until the swap expires on-chain (default: 60) |
amountOutMin | string | No | Set the minimum output yourself, in base units. Overrides both the quoted price and slippage |
maxSnipeTaxBps | number | No | The 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 |
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.05withdenominatedInQuote: "true"= buy with 0.05 ETH25withdenominatedInQuote: "true"on a USDG-quoted token = buy with 25 USDG
For sell transactions:
"100%"= sell your whole balance"50%"= sell half1000000withdenominatedInQuote: "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:
| Value | Meaning |
|---|---|
curve | Computed 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 |
pool | Computed 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 |
feed | The token's last executed trade, as the live feed saw it, priceAgeMs old |
logs | The 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 |
caller | You 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 block | Tax on the buy |
|---|---|
| 0 | 9900 bps (99%) |
| 1 | 618 bps (6.18%) |
| 2 | 19 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
expectedOutandamountOutMin; you are paying a cost you consented to, and the quote tells you the rate. - Tax above your ceiling, or no ceiling sent β HTTP
425withsnipeTaxBps,launchedAt,snipeTaxSeconds,snipeTaxEndsAtandretryAfterMs. SleepretryAfterMs, 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:
| Solana | Robinhood Chain | Why |
|---|---|---|
mint | token | EVM tokens are contracts, not mints |
denominatedInSol | denominatedInQuote | The quote asset is often not ETH |
| Binary response body | JSON | EVM has no canonical serialized unsigned transaction; sendTransaction takes fields |
priorityFee in SOL | priorityFee in gwei per gas | EVM prices gas per unit, not per transaction |
| No approvals | approvalTransaction | ERC-20 needs an allowance before anything can pull it |
| Blockhash expiry | deadline + nonce | Ordering and expiry work differently on an EVM chain |
Error Handling & Troubleshootingβ
| Error | Root Cause | Solution |
|---|---|---|
Missing required fields: publicKey, action, token, amount | A field is absent | Send all four |
action must be 'buy' or 'sell' | Invalid action | Use exactly "buy" or "sell" |
Invalid publicKey or token address | Not an EVM address | Check the address format |
No known route for this token | Not a PONS token, or a graduated one whose pool PumpDev has not seen trade | Verify 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 found | Selling more than you hold, or nothing at all | Check your balance |
Invalid percentage value β use 1% to 100% | A percentage outside the range | Use "50%" or "100%" |
Calculated sell amount is zero | The percentage rounds to nothing | Sell a larger share |
Price unavailable β pass amountOutMin explicitly | A 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 10000 | A fraction, a negative number or something that is not a number | Send whole basis points: 700 for 7% |
Token has graduated and its pool route is not known yet | The curve is done and the pool route has not been observed | Retry in a moment |
Refusing to build a trade with no floor⦠| The recent trades price the token too thinly to protect this amount | Trade a larger amount, or pass amountOutMin yourself |
Gas estimation failed: ... | The trade would revert on-chain | Read 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β
- PONS Create Token β launch your own token, client-signed
- PONS Claim Fees β claim your creator fees from the PONS escrow
- PONS WebSocket β react to launches and trades in real time
- Trading API β the same idea on Solana
- Fees β commission details
- PONS API overview β everything PumpDev offers on Robinhood Chain, on one page