DevelopersREST + WebSocket

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').

CORS: browsers only allow REST calls from TronPad's own origins. Server-side code, scripts and bots are not affected. A web app on another origin must call the API from its own backend. The WebSocket accepts any origin.

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
// 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:

shell
# 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: base64 data: 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.

response
{ "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.

own mint
// 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.

If the main transaction expires (past 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: BUY or SELL.
  • amount: a decimal string in UI units of what you pay in. For buys that is SOL or TRX (per payWith); for sells it is the token. Digits beyond the asset's decimals are truncated (SOL 9, TRX 6, tokens 6).
  • payWith: SOL or QUOTE (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 route says so and inAmount shows the TRX actually spent.
  • Graduating. While status is COMPLETE, quotes and builds return 409 until migration finishes.
  • Graduated. Once status is MIGRATED, trades route entirely through Jupiter (the DAMM v2 pool is indexed there). feeQuote reads 0 in 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/config
    Params: none
    Returns: LaunchpadConfigDTO

    Cluster, DBC config, quote token (mint, symbol, decimals), fees in bps (tradeFeeBps, reflectionBps, creatorBps, platformBps, protocolBps), curve.migrationQuoteThreshold, token supply/decimals, treasury. Cached 60 s.

  • GET/stats
    Params: none
    Returns: PlatformStatsDTO

    tokens, graduated, volume24hQuote, reflectedTotalQuote, quoteUsd.

Tokens

  • GET/tokens
    Params: sort new|volume|mcap|progress|lastTrade (new), status CURVE|COMPLETE|MIGRATED, q (name, symbol or exact mint, ≤64), creator, limit ≤100 (30), cursor
    Returns: { items: TokenDTO[], nextCursor }

    sort=progress only lists CURVE tokens unless you pass status.

  • GET/tokens/:mint
    Params: none
    Returns: TokenDTO

    Price, market cap, progress (0..1), status, quoteReserve, 24h stats, holders, feesTotalQuote, reflectedTotalQuote, pendingReflectionQuote. 404 if unknown.

  • GET/tokens/:mint/trades
    Params: limit ≤200 (50), cursor
    Returns: Paginated<TradeDTO>

    Newest first. venue is DBC (curve) or DAMM_V2 (graduated).

  • GET/tokens/:mint/candles
    Params: tf 1m|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/holders
    Params: limit ≤200 (50)
    Returns: HolderDTO[]

    share is 0..1 of total supply; label marks the curve, graduated pool, platform and creator.

  • GET/tokens/:mint/reflections
    Params: limit ≤100 (20), cursor
    Returns: Paginated<ReflectionEpochDTO>

    One row per fee claim: claimedQuote, holdersQuote, distributedQuote, carriedQuote, recipients, claimSignatures.

  • GET/trades/recent
    Params: limit ≤50 (20)
    Returns: TradeDTO[]

    All tokens, newest first. A plain array, not paginated.

Wallets

  • GET/wallets/:address/earnings
    Params: mint (optional filter)
    Returns: WalletEarningsResponse

    totalQuote plus per-token totalQuote, payouts, lastPaidAt. Confirmed payouts only.

  • GET/wallets/:address/reflections
    Params: limit ≤100 (30), cursor
    Returns: Paginated<ReflectionPayoutDTO> + totalQuote

    Every payout received, with its transaction signature.

  • GET/wallets/:address/trades
    Params: limit ≤100 (30), cursor
    Returns: Paginated<TradeDTO>

Launch

  • POST/launch/metadata
    Params: 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/build
    Params: creator, name, symbol, uri, mint?, description?, website?, twitter?, telegram?, imageUrl?, firstBuy?
    Returns: BuiltTransaction + mint, mintSigned

    Creator is fee payer. Simulated before it is returned.

Trade

  • GET/trade/quote
    Params: mint, side, amount, payWith, slippageBps (query string)
    Returns: TradeQuote

    Read-only. Shares the 60/min transaction-build limit.

  • POST/trade/build
    Params: same as quote + owner (JSON body)
    Returns: BuiltTransaction

    Owner is fee payer. Simulated before it is returned (except split pre-transaction flows).

RPC proxy, WebSocket, health

  • POST/rpc
    Params: JSON-RPC 2.0 request or batch (≤20)
    Returns: Upstream JSON-RPC response

    300 requests/min per IP. Allowlisted methods only (below).

  • WSwss://api.tronpad.xyz/ws
    Params: subscribe / unsubscribe / ping
    Returns: WsServerMessage

    See WebSocket below.

  • GET/health
    Params: none
    Returns: { 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:

sendTransactionsimulateTransactiongetLatestBlockhashgetSignatureStatusesgetBalancegetAccountInfogetMultipleAccountsgetTokenAccountsByOwnergetTokenAccountBalancegetSlotgetBlockHeightgetEpochInfogetFeeForMessagegetGenesisHashgetVersiongetMinimumBalanceForRentExemptiongetRecentPrioritizationFeesisBlockhashValid
shell
curl -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.

TopicEvents
tradestrade for every token: { trade: TradeDTO, token } with the token's new price, market cap, progress and status.
tokenstoken: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 }.

ws.ts
// 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

Read fees and thresholds from GET /config at runtime rather than hardcoding them. The values below are the current live ones.
ParameterCurrent value
fees.tradeFeeBps450: 4.5% of the TRX side of every trade
fees.reflectionBps230: 2.3% reflected to holders
fees.creatorBps70: 0.7% to the wallet that launched the token
fees.platformBps60: 0.6% platform
fees.protocolBps90: 0.9% Meteora protocol (20% of the fee, fixed)
quoteTRX on Solana, mint GbbesPbaYh5uiAZSYNXTc7w9jty1rpg3P9L4JeN4LkKc, 6 decimals
token1,000,000,000 supply, 6 decimals, no mint authority
curve.migrationQuoteThreshold≈44,288 TRX raised to graduate
  1. CURVE. Trades hit the Meteora Dynamic Bonding Curve. progress is the share of the graduation threshold raised.
  2. COMPLETE. The curve is full and the pool is migrating. Trading pauses (409) for a short time.
  3. MIGRATED. Liquidity moves to a Meteora DAMM v2 pool (dammPool on 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.

examples
// 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": ["…"] } }
StatusMeaning
400Validation failed. details is a list of { path, message } per field. Also amounts that round to zero.
404Unknown token or route, or the pool is not on chain yet.
409Mint address already in use, or the curve is complete and graduating (trading pauses until DAMM v2).
413Request body over 6 MB (the metadata image).
422Simulation 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.
429Rate limited. Read the RateLimit and RateLimit-Policy headers and back off.
502Upstream failure: Jupiter had no route, or IPFS pinning failed. Usually safe to retry.
503A 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.

ScopeLimit
Every endpoint600 / min / IP
/launch/build, /trade/build, /trade/quote60 / min / IP
/launch/metadata20 / hour / IP
/rpc300 / 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 the quote (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.