TronPad API
Launch tokens, quote and trade, and stream live activity from your own scripts and bots. The server builds unsigned Solana transactions; you sign them locally and send them.
Overview
- Base URL
- https://api.tronpad.xyz
- WebSocket
- wss://api.tronpad.xyz/ws
- Format
- JSON in and out. Amounts are decimal strings in UI units.
- Auth
- None. Every endpoint is public and rate limited per IP.
- Chain
- Solana mainnet. Pools are Meteora Dynamic Bonding Curve, then DAMM v2.
The quote token is TRX, wrapped as an SPL token on Solana. Fees, prices, reserves and reflections are all in TRX. Buys can be paid in SOL (routed through Jupiter inside the same transaction) or in TRX directly (payWith: 'QUOTE').
Quickstart
One file that launches a token (with an optional first buy) and buys an existing one. Node 18+, @solana/web3.js v1, and a funded keypair file from solana-keygen new -o wallet.json. It needs SOL for the launch rent, network fees and the first buy.
// tronpad.ts: launch a token and trade on TronPad from Node 18+.
// npm i @solana/web3.js@1
// npx tsx tronpad.ts launch ./logo.png
// npx tsx tronpad.ts buy <mint> 0.05
import { readFileSync } from 'node:fs'
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'
const API = 'https://api.tronpad.xyz'
// Any mainnet RPC works. The TronPad proxy is HTTP only, so we poll for confirmation.
const connection = new Connection(process.env.RPC_URL ?? `${API}/rpc`, 'confirmed')
// A solana-keygen JSON file. The key stays on this machine; the API never sees it.
const wallet = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(readFileSync(process.env.KEYPAIR ?? 'wallet.json', 'utf8'))),
)
type PayWith = 'SOL' | 'QUOTE'
type Quote = { inAmount: string; outAmount: string; minOut: string; feeQuote: string; priceImpactPct: number | null; route: string }
type Built = {
preTransactions?: string[]
transaction: string
lastValidBlockHeight: number
mint?: string
mintSigned?: boolean
quote: Quote | null
}
async function api<T>(path: string, body?: unknown): Promise<T> {
const res = await fetch(`${API}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
})
const json: unknown = await res.json()
if (!res.ok) {
const e = json as { error?: string; details?: unknown }
throw new Error(`${res.status} ${e.error ?? 'error'} ${e.details ? JSON.stringify(e.details) : ''}`)
}
return json as T
}
async function confirm(signature: string, timeoutMs = 90_000): Promise<void> {
const started = Date.now()
while (Date.now() - started < timeoutMs) {
const { value } = await connection.getSignatureStatuses([signature])
const st = value[0]
if (st?.err) throw new Error(`${signature} failed: ${JSON.stringify(st.err)}`)
if (st?.confirmationStatus === 'confirmed' || st?.confirmationStatus === 'finalized') return
await new Promise((r) => setTimeout(r, 1_000))
}
throw new Error(`${signature} not confirmed in time; build a fresh transaction and retry`)
}
async function signSendConfirm(b64: string): Promise<string> {
const tx = VersionedTransaction.deserialize(Buffer.from(b64, 'base64'))
// Adds the wallet's signature; a server-held mint signature is kept.
tx.sign([wallet])
const signature = await connection.sendRawTransaction(tx.serialize(), { maxRetries: 3 })
await confirm(signature)
return signature
}
/** preTransactions first, in order, each confirmed; then the main transaction. */
async function sendBuilt(built: Built): Promise<string> {
for (const pre of built.preTransactions ?? []) console.log('pre-transaction', await signSendConfirm(pre))
return signSendConfirm(built.transaction)
}
async function launch(imagePath: string): Promise<void> {
const token = {
name: 'My Token', // <= 32 bytes
symbol: 'MYTKN', // <= 10, letters and digits
description: 'Launched from a script',
website: 'https://example.com', // optional, https only
twitter: 'https://x.com/example',
}
// 1. Pin image + metadata to IPFS. The declared type is ignored; the server reads the bytes.
const image = `data:image/png;base64,${readFileSync(imagePath).toString('base64')}`
const meta = await api<{ uri: string; imageUrl: string }>('/launch/metadata', { ...token, image })
// 2. Build. No `mint`: the server assigns a …tron vanity address and signs for it.
const built = await api<Built>('/launch/build', {
creator: wallet.publicKey.toBase58(),
...token,
uri: meta.uri,
imageUrl: meta.imageUrl,
firstBuy: { amount: '0.1', payWith: 'SOL' as PayWith, slippageBps: 500 }, // optional
})
if (!built.mint || !built.mintSigned) throw new Error('server did not assign a mint')
console.log('mint', built.mint, 'first buy', built.quote)
// 3. Sign with the creator wallet only, send, confirm.
const signature = await sendBuilt(built)
console.log('launched', signature)
console.log(`https://tronpad.xyz/token/${built.mint}`)
}
async function buy(mint: string, amountSol: string): Promise<void> {
const params = { mint, side: 'BUY', amount: amountSol, payWith: 'SOL', slippageBps: '300' }
const quote = await api<Quote>(`/trade/quote?${new URLSearchParams(params)}`)
console.log('quote', quote)
const built = await api<Built>('/trade/build', { ...params, slippageBps: 300, owner: wallet.publicKey.toBase58() })
console.log('bought', await sendBuilt(built))
}
const [cmd, a, b] = process.argv.slice(2)
const run = cmd === 'launch' && a ? launch(a) : cmd === 'buy' && a && b ? buy(a, b) : null
if (!run) console.log('usage: launch <image> | buy <mint> <sol>')
run?.catch((err: unknown) => {
console.error(err instanceof Error ? err.message : err)
process.exit(1)
})The token page appears at https://tronpad.xyz/token/<mint> a few seconds after the launch confirms, once the indexer has picked it up. Read-only calls need nothing but HTTP:
# Live launchpad parameters (fees, quote mint, graduation threshold)
curl -s https://api.tronpad.xyz/config
# Top 10 tokens still on the curve, by 24h volume
curl -s 'https://api.tronpad.xyz/tokens?sort=volume&status=CURVE&limit=10'
# One token, its last 20 trades and 1h candles
curl -s https://api.tronpad.xyz/tokens/<mint>
curl -s 'https://api.tronpad.xyz/tokens/<mint>/trades?limit=20'
curl -s 'https://api.tronpad.xyz/tokens/<mint>/candles?tf=1h&limit=200'
# Quote a 0.05 SOL buy (read-only, nothing is built or signed)
curl -s 'https://api.tronpad.xyz/trade/quote?mint=<mint>&side=BUY&amount=0.05&payWith=SOL&slippageBps=300'Launch flow
1. Upload metadata
POST /launch/metadata pins the image and the Metaplex JSON to IPFS and returns { uri, imageUrl }. The metadata is immutable once the token launches.
image: base64data:URL, png, jpg, gif or webp, ≤4 MB decoded. The format is detected from the file bytes; the declared MIME type is ignored.name: ≤32 bytes UTF-8.symbol: ≤10 bytes, letters and digits; a leading$is stripped and it is upper-cased.description≤1000 chars.website,twitter,telegram:https://links, ≤200 chars, empty string means none.
2. Build the launch transaction
POST /launch/build with creator (your wallet), the same name, symbol and socials, the uri (and imageUrl) from step 1, and optionally firstBuy: { amount, payWith: 'SOL' | 'QUOTE', slippageBps }. The first buy lands atomically with pool creation, so nobody can buy before the creator.
Omit mint: the server assigns a vanity address ending in tron from its pool (a random address if the pool is empty), signs for it, and returns mintSigned: true. You sign only with the creator wallet. Each vanity address is handed out once; the same creator retrying within 10 minutes gets the same address back.
{ "transaction": "<base64 v0 tx>", "lastValidBlockHeight": 429320148,
"mint": "…tron", "mintSigned": true,
"quote": { "inAmount": "0.1", "outAmount": "…", "minOut": "…", "feeQuote": "…",
"priceImpactPct": 0, "route": "SOL > TRX (Jupiter) > Meteora DBC" },
"preTransactions": ["<base64 v0 tx>"] }To use your own mint (for example a vanity address you ground yourself), pass its public key as mint. The response has mintSigned: false and you must sign with both keypairs. The build fails with 409 if that address already exists on chain.
// Bring your own mint (e.g. your own vanity grind). You must co-sign with it.
const mintKeypair = Keypair.generate()
const built = await api<Built>('/launch/build', {
creator: wallet.publicKey.toBase58(),
mint: mintKeypair.publicKey.toBase58(),
...token,
uri: meta.uri,
imageUrl: meta.imageUrl,
})
// built.mintSigned === false
const tx = VersionedTransaction.deserialize(Buffer.from(built.transaction, 'base64'))
tx.sign([mintKeypair, wallet])3. Sign, send, confirm
Deserialize with VersionedTransaction.deserialize, sign with your wallet, send, and confirm. If the response has preTransactions, sign, send and confirm each one first, in order, and only then send transaction. This happens when a SOL leg does not fit in one transaction: the SOL→TRX swap lands first, then the create + first buy spends exactly what the swap guaranteed. The main transaction cannot be simulated before its pre-transaction lands, so the server only size-checks it.
lastValidBlockHeight) while you wait, call the build endpoint again for a fresh one. Do not re-send a pre-transaction that already confirmed.Trading
GET /trade/quote (query string) and POST /trade/build (JSON body, plus owner) take the same fields:
side:BUYorSELL.amount: a decimal string in UI units of what you pay in. For buys that is SOL or TRX (perpayWith); for sells it is the token. Digits beyond the asset's decimals are truncated (SOL 9, TRX 6, tokens 6).payWith:SOLorQUOTE(TRX). For sells it is what you receive.slippageBps: 10 to 5000, default 300 (3%).
The quote is a TradeQuote: inAmount, outAmount, minOut (after slippage), feeQuote (TRX), priceImpactPct and a readable route. Sending the built transaction works exactly like a launch, including preTransactions (for sells paid out in SOL, the curve sell lands first and the TRX→SOL swap second).
- Filling the curve. A buy larger than what is left on the curve is built as a partial fill: it buys exactly the remainder, which completes the curve, and the unused amount stays in your wallet. The
routesays so andinAmountshows the TRX actually spent. - Graduating. While
statusisCOMPLETE, quotes and builds return 409 until migration finishes. - Graduated. Once
statusisMIGRATED, trades route entirely through Jupiter (the DAMM v2 pool is indexed there).feeQuotereads0in that case, but the pool still charges its trading fee.
Endpoint reference
Paginated lists return { items, nextCursor }; pass nextCursor back as cursor until it is null. Defaults are in parentheses. Path params (:mint, :address) must be valid base58 Solana addresses.
Config & stats
- GET
/configParams: noneReturns:LaunchpadConfigDTOCluster, DBC config, quote token (
mint,symbol,decimals),feesin bps (tradeFeeBps,reflectionBps,creatorBps,platformBps,protocolBps),curve.migrationQuoteThreshold, token supply/decimals, treasury. Cached 60 s. - GET
/statsParams: noneReturns:PlatformStatsDTOtokens,graduated,volume24hQuote,reflectedTotalQuote,quoteUsd.
Tokens
- GET
/tokensParams:sortnew|volume|mcap|progress|lastTrade (new),statusCURVE|COMPLETE|MIGRATED,q(name, symbol or exact mint, ≤64),creator,limit≤100 (30),cursorReturns:{ items: TokenDTO[], nextCursor }sort=progressonly lists CURVE tokens unless you passstatus. - GET
/tokens/:mintParams: noneReturns:TokenDTOPrice, market cap,
progress(0..1),status,quoteReserve, 24h stats, holders,feesTotalQuote,reflectedTotalQuote,pendingReflectionQuote. 404 if unknown. - GET
/tokens/:mint/tradesParams:limit≤200 (50),cursorReturns:Paginated<TradeDTO>Newest first.
venueis DBC (curve) or DAMM_V2 (graduated). - GET
/tokens/:mint/candlesParams:tf1m|5m|15m|1h|4h|1d (5m),from,to(unix s),limit≤1000 (500)Returns:{ timeframe, items: CandleDTO[] }Ascending. OHLC in quote per token; empty list (not 404) for unknown mints.
- GET
/tokens/:mint/holdersParams:limit≤200 (50)Returns:HolderDTO[]shareis 0..1 of total supply;labelmarks the curve, graduated pool, platform and creator. - GET
/tokens/:mint/reflectionsParams:limit≤100 (20),cursorReturns:Paginated<ReflectionEpochDTO>One row per fee claim:
claimedQuote,holdersQuote,distributedQuote,carriedQuote,recipients,claimSignatures. - GET
/trades/recentParams:limit≤50 (20)Returns:TradeDTO[]All tokens, newest first. A plain array, not paginated.
Wallets
- GET
/wallets/:address/earningsParams:mint(optional filter)Returns:WalletEarningsResponsetotalQuoteplus per-tokentotalQuote,payouts,lastPaidAt. Confirmed payouts only. - GET
/wallets/:address/reflectionsParams:limit≤100 (30),cursorReturns:Paginated<ReflectionPayoutDTO>+totalQuoteEvery payout received, with its transaction
signature. - GET
/wallets/:address/tradesParams:limit≤100 (30),cursorReturns:Paginated<TradeDTO>
Launch
- POST
/launch/metadataParams:name,symbol,image,description?,website?,twitter?,telegram?Returns:{ uri, imageUrl }Pins the image and Metaplex JSON to IPFS. 20 uploads per hour per IP.
- POST
/launch/buildParams:creator,name,symbol,uri,mint?,description?,website?,twitter?,telegram?,imageUrl?,firstBuy?Returns:BuiltTransaction+mint,mintSignedCreator is fee payer. Simulated before it is returned.
Trade
- GET
/trade/quoteParams:mint,side,amount,payWith,slippageBps(query string)Returns:TradeQuoteRead-only. Shares the 60/min transaction-build limit.
- POST
/trade/buildParams: same as quote +owner(JSON body)Returns:BuiltTransactionOwner is fee payer. Simulated before it is returned (except split pre-transaction flows).
RPC proxy, WebSocket, health
- POST
/rpcParams: JSON-RPC 2.0 request or batch (≤20)Returns: Upstream JSON-RPC response300 requests/min per IP. Allowlisted methods only (below).
- WS
wss://api.tronpad.xyz/wsParams:subscribe/unsubscribe/pingReturns:WsServerMessageSee WebSocket below.
- GET
/healthParams: noneReturns:{ ok, db, redis, indexerLagSeconds }503 when the database or cache is down. indexerLagSeconds is how stale indexed data may be.
POST /rpc forwards to a private mainnet RPC, so you can send and confirm without your own provider. It is HTTP only: there are no websocket subscriptions, so confirmTransaction will not work; poll getSignatureStatuses instead (as the quickstart does). Other methods return 403 with a JSON-RPC -32601 error. Allowed methods:
sendTransactionsimulateTransactiongetLatestBlockhashgetSignatureStatusesgetBalancegetAccountInfogetMultipleAccountsgetTokenAccountsByOwnergetTokenAccountBalancegetSlotgetBlockHeightgetEpochInfogetFeeForMessagegetGenesisHashgetVersiongetMinimumBalanceForRentExemptiongetRecentPrioritizationFeesisBlockhashValidcurl -s https://api.tronpad.xyz/rpc -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getSignatureStatuses","params":[["<signature>"]]}'WebSocket
Connect to wss://api.tronpad.xyz/ws and send { op: 'subscribe', topics: [...] }. Up to 50 topics per socket; unknown topics are ignored. Send { op: 'ping' } every 25 s to keep the connection open; the server replies { op: 'pong' } and drops sockets that stop responding.
| Topic | Events |
|---|---|
| trades | trade for every token: { trade: TradeDTO, token } with the token's new price, market cap, progress and status. |
| tokens | token:new and token:update, each with a full TokenDTO. |
| token:<mint> | That token's trade, token:update and reflection ({ mint, epochId, distributedQuote, recipients }). |
Messages from the server are { op: "event", topic, event }, { op: "pong" } or { op: "error", message }.
// Browsers, Node 22+ (global WebSocket), or the `ws` package on older Node.
const ws = new WebSocket('wss://api.tronpad.xyz/ws')
ws.onopen = () => {
ws.send(JSON.stringify({ op: 'subscribe', topics: ['trades', 'tokens', `token:${mint}`] }))
setInterval(() => ws.send(JSON.stringify({ op: 'ping' })), 25_000)
}
ws.onmessage = (m) => {
const msg = JSON.parse(String(m.data))
if (msg.op !== 'event') return // 'pong' | 'error'
const e = msg.event
if (e.type === 'trade') console.log(e.trade.side, e.trade.quoteAmount, e.trade.symbol)
if (e.type === 'token:new') console.log('new token', e.token.mint)
if (e.type === 'reflection') console.log('paid holders', e.distributedQuote, e.mint)
}Fees & lifecycle
GET /config at runtime rather than hardcoding them. The values below are the current live ones.| Parameter | Current value |
|---|---|
| fees.tradeFeeBps | 450: 4.5% of the TRX side of every trade |
| fees.reflectionBps | 230: 2.3% reflected to holders |
| fees.creatorBps | 70: 0.7% to the wallet that launched the token |
| fees.platformBps | 60: 0.6% platform |
| fees.protocolBps | 90: 0.9% Meteora protocol (20% of the fee, fixed) |
| quote | TRX on Solana, mint GbbesPbaYh5uiAZSYNXTc7w9jty1rpg3P9L4JeN4LkKc, 6 decimals |
| token | 1,000,000,000 supply, 6 decimals, no mint authority |
| curve.migrationQuoteThreshold | ≈44,288 TRX raised to graduate |
- CURVE. Trades hit the Meteora Dynamic Bonding Curve.
progressis the share of the graduation threshold raised. - COMPLETE. The curve is full and the pool is migrating. Trading pauses (409) for a short time.
- MIGRATED. Liquidity moves to a Meteora DAMM v2 pool (
dammPoolon the token) and the LP is permanently locked. The same fee applies, and trades route via Jupiter.
Reflections work the same before and after graduation. The platform claims the fee, sends the holder share to every holder pro-rata in TRX, and publishes each epoch at /tokens/:mint/reflections. Payouts are pushed straight to wallets; there is no claim step. Shares too small to send carry over to the next epoch (carriedQuote, pendingReflectionQuote). Per-wallet totals are at /wallets/:address/earnings and /wallets/:address/reflections.
Errors & rate limits
Errors are { error: string, details?: unknown } with a 4xx or 5xx status. The /rpc proxy is the exception: it answers in JSON-RPC error format.
// 400: validation (details lists each field)
{ "error": "invalid request", "details": [{ "path": "amount", "message": "amount must be a positive decimal" }] }
// 422: the built transaction failed simulation
{ "error": "price moved beyond your slippage; try again or raise slippage", "details": { "logs": ["…"] } }| Status | Meaning |
|---|---|
| 400 | Validation failed. details is a list of { path, message } per field. Also amounts that round to zero. |
| 404 | Unknown token or route, or the pool is not on chain yet. |
| 409 | Mint address already in use, or the curve is complete and graduating (trading pauses until DAMM v2). |
| 413 | Request body over 6 MB (the metadata image). |
| 422 | Simulation failed (slippage, balance, missing SOL for fees), not enough liquidity, or route too large for one transaction. error is human-readable; details.logs holds the last program logs. |
| 429 | Rate limited. Read the RateLimit and RateLimit-Policy headers and back off. |
| 502 | Upstream failure: Jupiter had no route, or IPFS pinning failed. Usually safe to retry. |
| 503 | A dependency is down (uploads not configured, database or cache unavailable). |
Rate limits
Per IP, fixed windows. Every response carries RateLimit and RateLimit-Policy headers; a 429 means wait for the window to reset.
| Scope | Limit |
|---|---|
| Every endpoint | 600 / min / IP |
| /launch/build, /trade/build, /trade/quote | 60 / min / IP |
| /launch/metadata | 20 / hour / IP |
| /rpc | 300 / min / IP |
Safety
- Never send a private key to the API. No endpoint accepts one and the server never needs one. You only send public keys; signing happens on your machine.
- A bot signs whatever the server returns, so check it first: deserialize the transaction and confirm the fee payer (
message.staticAccountKeys[0]) is your wallet, and compare thequote(inAmount,minOut) against what you asked for. Cap trade sizes and slippage in your own code. - Use a dedicated hot wallet holding only what the bot needs. Keep the keypair file out of version control.
- Always double-check the mint address. Anyone can launch a token with any name and symbol.
New to TronPad? How it works covers the product side.