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.
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: 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:
# 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).imageUrlandimageMimefrom 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.
{ "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.
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:BUYorSELL.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
routesays so. - Graduating. While
statusisCOMPLETE, quotes and builds return 409 until migration finishes. - Graduated. Once
statusisMIGRATED, 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
/configParamsnoneReturnsLaunchpadConfigDTOCluster, DBC config, quote (SOL, 9 decimals),
feesin bps (tradeFeeBps,protocolBps,creatorBps,platformBps,buybackBps),buybackToken(mintis null until $PAY launches),siteUrl,curve.migrationQuoteThreshold, token supply/decimals. Cached 60 s. - GET
/statsParamsnoneReturnsPlatformStatsDTOtokens,graduated,volume24hQuote,buybackTotalQuote(SOL credited to fomo traders),matchedUsers,quoteUsd.
Tokens
- GET
/tokensParamssortnew|volume|mcap|progress|lastTrade|lastBuy (new),statusCURVE|COMPLETE|MIGRATED,q(name, symbol, exact mint or fomo handle, ≤64),match(fomo handle),creator,limit≤100 (30),cursorReturns{ items: TokenDTO[], nextCursor }sort=progressonly lists CURVE tokens unless you passstatus.lastBuyis what the site calls trending. - GET
/tokens/:mintParamsnoneReturnsTokenDTOPrice, market cap,
progress(0..1),status,quoteReserve, 24h stats, holders,feesTotalQuote,match(the fomo user:handle,displayName,avatarUrl,verified) andbuybackQuote(SOL credited to them). 404 if unknown. - GET
/tokens/:mint/tradesParamslimit≤200 (50),cursorReturnsPaginated<TradeDTO>Newest first.
venueis DBC (curve) or DAMM_V2 (graduated). - GET
/tokens/:mint/candlesParamstf1m|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/holdersParamslimit≤200 (50)ReturnsHolderDTO[]shareis 0..1 of total supply;labelmarks the curve, graduated pool, platform and creator. - GET
/tokens/:mint/feesParamslimit≤100 (20),cursorReturnsPaginated<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/recentParamslimit≤50 (20)ReturnsTradeDTO[]All tokens, newest first. A plain array, not paginated.
fomo users
- GET
/fomo/searchParamsq(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/usersParamslimit≤50 (12)ReturnsFomoUserDTO[]Traders with at least one coin, ranked by SOL bought back for them.
- GET
/fomo/users/:handleParamsnoneReturns{ 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/createdParamsnoneReturns{ 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/tradesParamslimit≤100 (30),cursorReturnsPaginated<TradeDTO>
Launch, trade, claim
- POST
/launch/imageParamsimage(data: URL)Returns{ imageUrl, imageMime }Pins the image to IPFS. 20 uploads per hour per IP.
- POST
/launch/buildParamscreator,match(fomo handle),name,symbol,imageUrl,imageMime?,description?,twitter?,telegram?,firstBuy?ReturnsBuiltTransaction+mint,mintSigned: trueChecks the handle on fomo.family, assigns a …fomo mint, pins the metadata and signs for the mint. The creator is fee payer.
- GET
/trade/quoteParamsmint,side,amount,payWith,slippageBps(query string)ReturnsTradeQuoteRead-only. Shares the 60/min transaction-build limit.
- POST
/trade/buildParamssame as quote +owner(JSON body)ReturnsBuiltTransactionOwner is fee payer. Simulated before it is returned.
- POST
/creator/claimParamscreator,mintReturnsBuiltTransaction403 unless creator is the wallet that launched the coin. Withdraws its creator fees (curve, and LP after graduation).
RPC proxy, WebSocket, health
- POST
/rpcParamsJSON-RPC 2.0 request or batch (≤20)ReturnsUpstream JSON-RPC response300 requests/min per IP. Allowlisted methods only (below).
- WS
wss://api.payfomo.family/wsParamssubscribe/unsubscribe/pingReturnsWsServerMessageSee WebSocket below.
- GET
/healthParamsnoneReturns{ 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
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.
| Topic | Events |
|---|---|
| trades | trade for every token: { trade: TradeDTO, token } with the new price, market cap, progress and status. |
| tokens | token: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 }.
// 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
GET /config at runtime rather than hardcoding them. The values below are the defaults.| Parameter | Default |
|---|---|
| fees.tradeFeeBps | 250: 2.5% of the SOL side of every trade |
| fees.buybackBps | 100: 1% bought back as $PAY for the matched fomo user |
| fees.creatorBps | 50: 0.5% to the wallet that launched the coin (claimed) |
| fees.platformBps | 50: 0.5% platform treasury |
| fees.protocolBps | 50: 0.5% Meteora protocol (20% of the fee, fixed) |
| buybackToken | { symbol: PAY, mint: null until it launches (SOL accrues) } |
| token | 1,000,000,000 supply, 6 decimals, no mint authority |
| curve.migrationQuoteThreshold | SOL 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 (
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.
// 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" }| Status | Meaning |
|---|---|
| 400 | Validation failed. details is a list of { path, message } per field. Also amounts that round to zero. |
| 403 | POST /creator/claim from a wallet that did not launch the coin. |
| 404 | Unknown token, unknown fomo handle (launch, /fomo/users/:handle), or the pool is not on chain yet. |
| 409 | The curve is complete and graduating (trading pauses until DAMM v2). |
| 413 | Request body over 6 MB (the image). |
| 422 | Simulation failed (slippage, balance, missing SOL for fees) or not enough liquidity. 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, fomo.family 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, /creator/claim | 60 / min / IP |
| /launch/image | 20 / hour / IP |
| /fomo/search | 90 / min / IP |
| /rpc | 300 / 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 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. A payfomo coin's mint ends in
fomo.
New to payfomo? How it works covers the product side.