Developers · REST + WebSocket

API

Launch coins for fomo traders, quote and trade, claim creator fees and stream live activity from your own scripts and bots. The server builds Solana transactions; you sign them locally and send them.

01

Overview

Base URL
https://api.payfomo.family
WebSocket
wss://api.payfomo.family/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 quoted in SOL, then DAMM v2.

Every coin is launched for a fomo.family user (the match) and every mint ends in fomo. Fees, prices and reserves are in SOL (9 decimals). payWith is SOL; QUOTE means the same thing now that the quote is SOL.

CORS: browsers only allow REST calls from payfomo'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.

02

Quickstart

One file that launches a coin for a fomo trader (with an optional first buy), buys an existing one and claims creator fees. Node 18+, @solana/web3.js v1, and a funded keypair file from solana-keygen new -o wallet.json.

payfomo.ts
// payfomo.ts: launch a coin for a fomo trader, buy one, claim creator fees. Node 18+.
//   npm i @solana/web3.js@1
//   npx tsx payfomo.ts launch unipcs ./logo.png
//   npx tsx payfomo.ts buy <mint> 0.05
//   npx tsx payfomo.ts claim <mint>
import { readFileSync } from 'node:fs'
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'

const API = 'https://api.payfomo.family'
// Any mainnet RPC works. The payfomo 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 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; the server's mint signature (launches) 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(handle: string, imagePath: string): Promise<void> {
  // 1. Pin the image. The server reads the bytes; the declared type is ignored.
  const image = `data:image/png;base64,${readFileSync(imagePath).toString('base64')}`
  const { imageUrl, imageMime } = await api<{ imageUrl: string; imageMime: string }>('/launch/image', { image })

  // 2. Build. `match` is the fomo handle; the server checks it on fomo.family, assigns a …fomo
  //    vanity mint, pins the metadata (website = the coin's payfomo page) and signs for the mint.
  const built = await api<Built>('/launch/build', {
    creator: wallet.publicKey.toBase58(),
    match: handle,
    name: 'Cat Coin', // <= 32 bytes
    symbol: 'CAT', // <= 10, letters and digits
    description: 'Launched from a script',
    twitter: 'https://x.com/example', // optional, https only
    imageUrl,
    imageMime,
    firstBuy: { amount: '0.1', payWith: 'SOL', 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.
  console.log('launched', await sendBuilt(built))
  console.log(`https://payfomo.family/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))
}

/** Withdraw this wallet's creator fees from one coin it launched. */
async function claim(mint: string): Promise<void> {
  const built = await api<Built>('/creator/claim', { creator: wallet.publicKey.toBase58(), mint })
  console.log('claimed', await sendBuilt(built))
}

const [cmd, a, b] = process.argv.slice(2)
const run =
  cmd === 'launch' && a && b ? launch(a, b) : cmd === 'buy' && a && b ? buy(a, b) : cmd === 'claim' && a ? claim(a) : null
if (!run) console.log('usage: launch <fomo handle> <image> | buy <mint> <sol> | claim <mint>')
run?.catch((err: unknown) => {
  console.error(err instanceof Error ? err.message : err)
  process.exit(1)
})

The coin page appears at https://payfomo.family/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 (fee split, buyback token, graduation threshold)
curl -s https://api.payfomo.family/config

# Coins still on the curve, by 24h volume
curl -s 'https://api.payfomo.family/tokens?sort=volume&status=CURVE&limit=10'

# Every coin made for one fomo trader, and their page
curl -s 'https://api.payfomo.family/tokens?match=unipcs'
curl -s https://api.payfomo.family/fomo/users/unipcs

# One coin, its last 20 trades, 1h candles and fee history
curl -s https://api.payfomo.family/tokens/<mint>
curl -s 'https://api.payfomo.family/tokens/<mint>/trades?limit=20'
curl -s 'https://api.payfomo.family/tokens/<mint>/candles?tf=1h&limit=200'
curl -s https://api.payfomo.family/tokens/<mint>/fees

# Quote a 0.05 SOL buy (read-only, nothing is built or signed)
curl -s 'https://api.payfomo.family/trade/quote?mint=<mint>&side=BUY&amount=0.05&payWith=SOL&slippageBps=300'

03

Launch flow

1. Pick the fomo trader

GET /fomo/search?q= returns matching fomo.family traders. Pass the chosen handle as match when you build; the server checks it exists on fomo.family (404 otherwise) and stores the latest name and avatar.

2. Upload the image

POST /launch/image with { image }, a base64 data: URL (png, jpg, gif or webp, ≤4 MB decoded; the format is read from the bytes). It returns { imageUrl, imageMime }.

3. Build the launch transaction

  • creator: your wallet (fee payer and pool creator). match: the fomo handle.
  • name ≤32 bytes UTF-8; symbol ≤10 bytes, letters and digits (a leading $ is stripped, upper-cased).
  • imageUrl and imageMime from step 2; description ≤1000 chars; twitter, telegram: https:// links. There is no website field: the metadata's website is the coin's payfomo page.
  • firstBuy: { amount, payWith: 'SOL', slippageBps } (optional) lands atomically with pool creation, so nobody buys before you.

The server assigns a vanity mint ending in fomo, pins the metadata, signs for the mint and returns mintSigned: true. You sign only with the creator wallet.

response
{ "transaction": "<base64 v0 tx>", "lastValidBlockHeight": 429320148,
  "mint": "…fomo", "mintSigned": true,
  "quote": { "inAmount": "0.1", "outAmount": "…", "minOut": "…", "feeQuote": "…",
             "priceImpactPct": 0, "route": "Meteora DBC" } }

4. 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.

If the main transaction expires (past lastValidBlockHeight) while you wait, build again for a fresh one. Do not re-send a pre-transaction that already confirmed.

04

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: SOL for buys, the token for sells. Digits beyond the asset's decimals are truncated (SOL 9, tokens 6).
  • payWith: SOL. slippageBps: 10 to 5000, default 300 (3%).

The quote is a TradeQuote: inAmount, outAmount, minOut (after slippage), feeQuote (SOL), priceImpactPct and a readable route.

  • Filling the curve. A buy larger than what is left on the curve buys exactly the remainder; the unused SOL stays in your wallet. The route says so.
  • Graduating. While status is COMPLETE, quotes and builds return 409 until migration finishes.
  • Graduated. Once status is MIGRATED, trades route through Jupiter to the DAMM v2 pool.

05

Creator claims

GET /wallets/:address/created lists the coins a wallet launched with the creator fee each can pay out now (claimableQuote) and the total. To withdraw one, POST /creator/claim with { creator, mint }, then sign and send the returned transaction with the creator wallet. Any other wallet gets 403.

06

Endpoint reference

Paginated lists return { items, nextCursor }; pass nextCursor back as cursor until it is null. Defaults are in parentheses. Path params :mint and :address must be valid base58 Solana addresses.

Config & stats

  • GET/config
    Paramsnone
    ReturnsLaunchpadConfigDTO

    Cluster, DBC config, quote (SOL, 9 decimals), fees in bps (tradeFeeBps, protocolBps, creatorBps, platformBps, buybackBps), buybackToken (mint is null until $PAY launches), siteUrl, curve.migrationQuoteThreshold, token supply/decimals. Cached 60 s.

  • GET/stats
    Paramsnone
    ReturnsPlatformStatsDTO

    tokens, graduated, volume24hQuote, buybackTotalQuote (SOL credited to fomo traders), matchedUsers, quoteUsd.

Tokens

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

    sort=progress only lists CURVE tokens unless you pass status. lastBuy is what the site calls trending.

  • GET/tokens/:mint
    Paramsnone
    ReturnsTokenDTO

    Price, market cap, progress (0..1), status, quoteReserve, 24h stats, holders, feesTotalQuote, match (the fomo user: handle, displayName, avatarUrl, verified) and buybackQuote (SOL credited to them). 404 if unknown.

  • GET/tokens/:mint/trades
    Paramslimit ≤200 (50), cursor
    ReturnsPaginated<TradeDTO>

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

  • GET/tokens/:mint/candles
    Paramstf 1m|5m|15m|1h|4h|1d (5m), from, to (unix s), limit ≤1000 (500)
    Returns{ timeframe, items: CandleDTO[] }

    Ascending. OHLC in SOL per token; empty list (not 404) for unknown mints.

  • GET/tokens/:mint/holders
    Paramslimit ≤200 (50)
    ReturnsHolderDTO[]

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

  • GET/tokens/:mint/fees
    Paramslimit ≤100 (20), cursor
    ReturnsPaginated<FeeEpochDTO>

    One row per platform fee claim: claimedQuote, platformQuote, buybackQuote (credited to the matched user), claimSignatures. The creator's share is not in these; it waits in the pool.

  • GET/trades/recent
    Paramslimit ≤50 (20)
    ReturnsTradeDTO[]

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

fomo users

  • GET/fomo/search
    Paramsq (name or handle, ≤40)
    ReturnsFomoSearchHitDTO[]

    fomo.family traders for a launch picker: handle, displayName, avatarUrl, verified, followers. Up to 12, cached a minute. Debounce it.

  • GET/fomo/users
    Paramslimit ≤50 (12)
    ReturnsFomoUserDTO[]

    Traders with at least one coin, ranked by SOL bought back for them.

  • GET/fomo/users/:handle
    Paramsnone
    Returns{ user: FomoUserDTO, deliveries: DeliveryDTO[] }

    tradingWallet (null until found), tokens, creditedQuote, owedQuote (SOL waiting for the next buyback), pendingBuyback / sentBuyback ($PAY held / delivered) and the latest deliveries. 404 if no coin was launched for them yet.

Wallets

  • GET/wallets/:address/created
    Paramsnone
    Returns{ items: CreatedTokenDTO[], claimableTotalQuote }

    Coins the wallet launched, each with claimableQuote: the creator fee waiting in its pool right now, read on chain.

  • GET/wallets/:address/trades
    Paramslimit ≤100 (30), cursor
    ReturnsPaginated<TradeDTO>

Launch, trade, claim

  • POST/launch/image
    Paramsimage (data: URL)
    Returns{ imageUrl, imageMime }

    Pins the image to IPFS. 20 uploads per hour per IP.

  • POST/launch/build
    Paramscreator, match (fomo handle), name, symbol, imageUrl, imageMime?, description?, twitter?, telegram?, firstBuy?
    ReturnsBuiltTransaction + mint, mintSigned: true

    Checks the handle on fomo.family, assigns a …fomo mint, pins the metadata and signs for the mint. The creator is fee payer.

  • GET/trade/quote
    Paramsmint, side, amount, payWith, slippageBps (query string)
    ReturnsTradeQuote

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

  • POST/trade/build
    Paramssame as quote + owner (JSON body)
    ReturnsBuiltTransaction

    Owner is fee payer. Simulated before it is returned.

  • POST/creator/claim
    Paramscreator, mint
    ReturnsBuiltTransaction

    403 unless creator is the wallet that launched the coin. Withdraws its creator fees (curve, and LP after graduation).

RPC proxy, WebSocket, health

  • POST/rpc
    ParamsJSON-RPC 2.0 request or batch (≤20)
    ReturnsUpstream JSON-RPC response

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

  • WSwss://api.payfomo.family/ws
    Paramssubscribe / unsubscribe / ping
    ReturnsWsServerMessage

    See WebSocket below.

  • GET/health
    Paramsnone
    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:

sendTransaction · simulateTransaction · getLatestBlockhash · getSignatureStatuses · getBalance · getAccountInfo · getMultipleAccounts · getTokenAccountsByOwner · getTokenAccountBalance · getSlot · getBlockHeight · getEpochInfo · getFeeForMessage · getGenesisHash · getVersion · getMinimumBalanceForRentExemption · getRecentPrioritizationFees · isBlockhashValid

shell
curl -s https://api.payfomo.family/rpc -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"getSignatureStatuses","params":[["<signature>"]]}'

07

WebSocket

Connect to wss://api.payfomo.family/ws and send { op: 'subscribe', topics: [...] }. Up to 50 topics per socket; unknown topics are ignored. Send { op: 'ping' } every 25 s; the server replies { op: 'pong' } and drops sockets that stop responding.

TopicEvents
tradestrade for every token: { trade: TradeDTO, token } with the new price, market cap, progress and status.
tokenstoken:new and token:update, each with a full TokenDTO.
token:<mint>That token's trade and token:update, and buyback: { mint, epochId, handle, buybackQuote } when a fee claim credits the matched user.

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.payfomo.family/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 coin', e.token.mint, 'for', e.token.match?.handle)
  if (e.type === 'buyback') console.log('bought back', e.buybackQuote, 'SOL for', e.handle, 'from', e.mint)
}

08

Fees & lifecycle

Read fees and thresholds from GET /config at runtime rather than hardcoding them. The values below are the defaults.
ParameterDefault
fees.tradeFeeBps250: 2.5% of the SOL side of every trade
fees.buybackBps100: 1% bought back as $PAY for the matched fomo user
fees.creatorBps50: 0.5% to the wallet that launched the coin (claimed)
fees.platformBps50: 0.5% platform treasury
fees.protocolBps50: 0.5% Meteora protocol (20% of the fee, fixed)
buybackToken{ symbol: PAY, mint: null until it launches (SOL accrues) }
token1,000,000,000 supply, 6 decimals, no mint authority
curve.migrationQuoteThresholdSOL 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) and the LP is permanently locked, split creator 25% / platform 75% so the fee shares stay the same.

The platform claims its side every few minutes (see /tokens/:mint/fees), keeps the platform share and credits the rest to the matched user, which a buyback wallet spends on $PAY and delivers to their fomo wallet (/fomo/users/:handle).

09

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": "symbol", "message": "symbol is letters and digits only" }] }

// 404: the fomo handle does not exist
{ "error": "fomo.family has no user @someone" }

// 403: claiming another wallet's creator fees
{ "error": "only the wallet that launched this token can claim its creator fees" }
StatusMeaning
400Validation failed. details is a list of { path, message } per field. Also amounts that round to zero.
403POST /creator/claim from a wallet that did not launch the coin.
404Unknown token, unknown fomo handle (launch, /fomo/users/:handle), or the pool is not on chain yet.
409The curve is complete and graduating (trading pauses until DAMM v2).
413Request body over 6 MB (the image).
422Simulation failed (slippage, balance, missing SOL for fees) or not enough liquidity. 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, fomo.family 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/quote, /creator/claim60 / min / IP
/launch/image20 / hour / IP
/fomo/search90 / min / IP
/rpc300 / min / IP

10

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. A payfomo coin's mint ends in fomo.

New to payfomo? How it works covers the product side.

payfomo

Launch a coin for any fomo trader. Coins are created by independent users, are highly speculative and can lose all value. Nothing here is financial advice. A payfomo mint always ends in …fomo.

© 2026 payfomo · payfomo.familySolana · Meteora DBC · DAMM v2