The Pump.fun first buyers API in Solana Tracker is GET /v2/pnl/tokens/{token}/first-buyers: it lists the earliest wallets to buy a token in chronological order, each with token-scoped PnL, lifetime wallet PnL, current position, timing and identity labels. Combine it with the sniper, insider and bundler data on the same token to see which early buyers were flagged, who is still holding and who already sold. Read the numbers as a description of what happened, not as proof of anyone's intent.
This guide covers the endpoint and its SDK method, how it differs from the older /first-buyers/{token} route, a complete TypeScript report, live sniper and insider rooms, and how to interpret early-buyer PnL without fooling yourself. The data is part of the Pump.fun API, a view of the Solana Data API; keys and plans are in the Data API dashboard.
When to use the Pump.fun first buyers API
Use it when the question is "who got in first, and what did they do next?": launch post-mortems, sniper screening before you trade a fresh token, copy-trading research, or a risk panel next to a chart. It answers with one request per page instead of replaying the token's trade history yourself.
Pick the endpoint by the question:
| Question | Endpoint | SDK method | Ordering |
|---|---|---|---|
| Who bought first and what happened to them? | GET /v2/pnl/tokens/{token}/first-buyers | getPnlV2TokenFirstBuyers | Earliest first only |
| Who made or lost the most on this token? | GET /v2/pnl/tokens/{token}/traders | getPnlV2TokenTraders | Sortable by PnL, ROI, size, latest trade |
| Which wallets are flagged snipers or insiders, and how much do they hold? | GET /tokens/{token} (risk block) | getTokenInfo | Aggregates plus wallet lists |
| Which wallets bought in bundles? | GET /tokens/{token}/bundlers | getTokenBundlers | Up to the top 500 bundler wallets |
| How are those holdings changing right now? | Datastream sniper:, insider:, bundlers: rooms | subscribe.snipers / insiders / bundlers | Push |
The first-buyers endpoint rejects leaderboard sorting: only sort=first_trade and direction=asc are accepted. If you want the most profitable traders, call the traders endpoint instead of re-sorting a first-buyers page.
The legacy /first-buyers/{token} route
The original GET /first-buyers/{token} (SDK getFirstBuyers) still exists but is marked deprecated in the docs, which point to the PnL V2 route above. It returns a flat array of up to the first 100 buyers since the API started recording data, with snake_case fields such as first_buy_time, held, sold, realized, unrealized, total_invested and current_value. New code should use PnL V2: it adds wallet-level PnL, identity labels, pagination and filters.
Response shape
getPnlV2TokenFirstBuyers(mint, { limit }) returns { meta, traders, pagination }. One trader, trimmed from the documented example:
{
"meta": { "symbol": "TRUMP", "decimals": 6, "price": 2.8078, "marketCap": 2807853276.49, "liquidity": 66749438.29, "primaryMarket": "meteora-dlmm" },
"traders": [
{
"wallet": "6QSc2CxSdkUQSXttkceR9yMuxMf36L75fS8624wJ9tXv",
"pnl": {
"token": { "realized": 0, "unrealized": 0, "total": 0 },
"wallet": { "realized": 58523.47, "unrealized": -11351.36, "total": 47172.11, "totalTrades": 13111, "tokensTraded": 2258 }
},
"identity": { "type": "axiom", "tags": ["axiom"], "platforms": ["axiom"] },
"position": { "balance": 0, "costBasis": 1096109, "value": 0, "price": 2.8078 },
"counts": { "buys": 1, "sells": 0, "total": 1 },
"roi": 0,
"invested": 1096109,
"proceeds": 0,
"current": { "balance": 0, "costBasis": 1096109, "value": 0, "price": 2.8078, "avgCost": 2.8079 },
"timing": { "firstTrade": 1737165693000, "lastTrade": 1737165693000, "firstSell": null, "lastSell": null, "holdTimeSecs": 0 }
}
],
"pagination": { "hasMore": true, "nextCursor": "eyJzb3J0VmFsdWUiOiIxNzM3MTY1Njk0MDAwIiwidGllQnJlYWtlciI6MTAwNzQ1OX0=", "count": 2, "total": 2 }
}
The fields that matter most:
pnl.tokenis PnL on this mint only:realized,unrealized,totalin USD.pnl.walletis the wallet's lifetime PnL across every token. It is context, not performance on this launch.current(and the legacypositionwith the same values) holds balance, cost basis and value.roiis a percentage (170.45means +170.45%).timingvalues are Unix milliseconds exceptholdTimeSecs; sell fields arenullwhen the wallet never sold.identitycarries labels such astype,tags,platformsand, on token-scoped endpoints,poolanddeveloperroles resolved for this exact token.
Many numeric fields are nullable. Treat null as unknown, never as zero.
Build a Pump.fun first buyers report in TypeScript
The script fetches the first buyers, the token's risk block and the bundler list in parallel, marks each wallet S (sniper), I (insider) or B (bundler), classifies its position, and prints a cohort summary. Risk and bundler data are optional enrichment, so a failure there does not stop the report.
npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
.env.example:
# Data API key from /account/data-api
ST_API_KEY=
# Pump.fun token mint to analyze (base58)
TOKEN_MINT=
# First buyers per request, 1-200 (default 50)
FIRST_BUYERS_LIMIT=50
index.ts:
import { Client, DataApiError, RateLimitError, type PnlV2Holder } from "@solana-tracker/data-api";
const apiKey = process.env.ST_API_KEY?.trim();
const mint = process.env.TOKEN_MINT?.trim();
const LIMIT = Number(process.env.FIRST_BUYERS_LIMIT ?? "50");
if (!apiKey) exit("Missing ST_API_KEY (Data API key from /account/data-api)");
if (!mint || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) exit("Set TOKEN_MINT to a base58 Pump.fun mint");
if (!Number.isInteger(LIMIT) || LIMIT < 1 || LIMIT > 200) exit("FIRST_BUYERS_LIMIT must be an integer from 1 to 200");
function exit(message: string): never {
console.error(message);
process.exit(1);
}
const client = new Client({ apiKey });
// The REST client has no timeout or retry of its own.
async function withRetry<T>(label: string, fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
let timer: NodeJS.Timeout | undefined;
try {
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out`)), 15_000);
});
return await Promise.race([fn(), timeout]);
} catch (error) {
const retryable =
error instanceof RateLimitError || !(error instanceof DataApiError) || error.status === undefined || error.status >= 500;
if (!retryable || attempt >= attempts) throw error;
const retryAfterMs = error instanceof RateLimitError && error.retryAfter ? error.retryAfter * 1000 : 0;
await new Promise((r) => setTimeout(r, Math.max(500 * 2 ** (attempt - 1), retryAfterMs)));
} finally {
clearTimeout(timer);
}
}
}
// Token-scoped state only. A zero balance with no sells means the tokens were transferred out.
function state(t: PnlV2Holder): string {
const balance = t.current?.balance ?? t.position?.balance;
if (balance == null) return "unknown";
if (balance > 0) return "holding";
if (t.counts.sells === 0) return "moved out";
const realized = t.pnl.token.realized ?? 0;
return realized > 0 ? "exited +" : realized < 0 ? "exited -" : "exited 0";
}
const usd = (n: number | null | undefined) => (n == null ? "n/a" : `$${n.toFixed(0)}`);
async function main(token: string): Promise<void> {
const [buyers, info, bundlers] = await Promise.all([
withRetry("first buyers", () => client.getPnlV2TokenFirstBuyers(token, { limit: LIMIT })),
withRetry("token info", () => client.getTokenInfo(token)).catch(() => undefined),
withRetry("bundlers", () => client.getTokenBundlers(token)).catch(() => undefined),
]);
// Cross-reference sets: risk.snipers / risk.insiders wallets and the bundlers endpoint.
const ids = (list?: Array<{ wallet?: string; address: string }>) => new Set((list ?? []).map((w) => w.wallet ?? w.address));
const snipers = ids(info?.risk.snipers.wallets);
const insiders = ids(info?.risk.insiders.wallets);
const bundled = new Set((bundlers?.wallets ?? []).map((w) => w.wallet));
const { meta, traders, pagination } = buyers;
console.log(`${meta.symbol ?? "?"} mcap ${usd(meta.marketCap)} market ${meta.primaryMarket ?? "n/a"} risk ${info?.risk.score ?? "?"}/10`);
traders.forEach((t, i) => {
const flags = `${snipers.has(t.wallet) ? "S" : ""}${insiders.has(t.wallet) ? "I" : ""}${bundled.has(t.wallet) ? "B" : ""}` || "-";
console.log(
[
String(i + 1).padStart(3),
t.wallet,
new Date(t.timing.firstTrade ?? 0).toISOString().slice(0, 19),
`in ${usd(t.invested)}`,
`token pnl ${usd(t.pnl.token.total)}`,
`roi ${t.roi == null ? "n/a" : `${t.roi.toFixed(0)}%`}`,
state(t).padEnd(9),
`lifetime ${usd(t.pnl.wallet?.total)}`,
t.identity?.name ?? t.identity?.type ?? "",
flags,
].join(" "),
);
});
const states = traders.map(state);
const count = (s: string) => states.filter((x) => x === s).length;
console.log(
`\n${traders.length} of ${pagination.total} first buyers: holding ${count("holding")}, exited + ${count("exited +")}, ` +
`exited - ${count("exited -")}, moved out ${count("moved out")}${pagination.hasMore ? " (more pages)" : ""}`,
);
}
main(mint).catch((error: unknown) => {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
});
Each request counts against your plan. Pagination uses pagination.nextCursor; pass it back as cursor to read the next page.
Watch sniper, insider and bundler changes live
The report is a snapshot. To see the flagged groups move, Datastream has per-token rooms: sniper:{token}, insider:{token} and bundlers:{token}. Each message describes one wallet's change, for example:
{
"wallet": "WalletAddressHere",
"amount": "1000000000",
"tokenAmount": 1000000,
"percentage": 0.15,
"previousAmount": 500000,
"previousPercentage": 0.075,
"totalSniperPercentage": 5.25,
"totalInsiderPercentage": 12.5
}
Bundler messages add boughtAmount, boughtPercentage and totalBundlerPercentage. With ST_DATASTREAM_KEY set (Premium plan or higher), a standalone watcher looks like this:
import { Datastream } from "@solana-tracker/data-api";
const streamKey = process.env.ST_DATASTREAM_KEY?.trim();
const mint = process.env.TOKEN_MINT?.trim();
if (!streamKey || !mint) {
console.error("Set ST_DATASTREAM_KEY and TOKEN_MINT");
process.exit(1);
}
const wsUrl = streamKey.startsWith("wss://") ? streamKey : `wss://datastream.solanatracker.io/${streamKey}`;
const ds = new Datastream({ wsUrl, autoReconnect: true, reconnectDelay: 1_000, reconnectDelayMax: 30_000, randomizationFactor: 0.5 });
ds.on("error", () => console.error("[datastream] connection error")); // never log wsUrl
const move = (prev: number, now: number) => `${prev.toFixed(2)}% -> ${now.toFixed(2)}%`;
const subs = [
ds.subscribe.snipers(mint).on((u) =>
console.log(`SNIPER ${u.wallet} ${move(u.previousPercentage, u.percentage)} all snipers ${u.totalSniperPercentage.toFixed(2)}%`),
),
ds.subscribe.insiders(mint).on((u) =>
console.log(`INSIDER ${u.wallet} ${move(u.previousPercentage, u.percentage)} all insiders ${u.totalInsiderPercentage.toFixed(2)}%`),
),
ds.subscribe.bundlers(mint).on((u) =>
console.log(`BUNDLER ${u.wallet} ${move(u.previousPercentage, u.percentage)} all bundlers ${u.totalBundlerPercentage.toFixed(2)}%`),
),
];
process.once("SIGINT", () => {
for (const sub of subs) sub.unsubscribe();
ds.disconnect();
process.exit(0);
});
The SDK reconnects with capped exponential backoff and rejoins all three rooms. Never log wsUrl: it contains your key.
Interpret early-buyer PnL honestly
First-buyer data is easy to over-read. The rules that keep conclusions defensible:
- Token PnL is not skill. A wallet up 1,800% on one launch may be down overall. Compare
pnl.tokenwithpnl.walletbefore calling anyone a good trader, and remember that lifetime PnL also includes luck, survivorship and wallets that rotate. - Unrealized is a mark, not cash. Holders'
unrealizeduses the current price. On thin Pump.fun liquidity, selling the position would move the price, so the realizable value is lower. - Zero balance does not always mean sold. The documented example above shows a wallet with one buy, no sells and a zero balance: the tokens left by transfer. Token PnL for that wallet says little; follow the receiving wallet instead. The script labels these "moved out".
- Defaults hide rows.
excludeArbitrageandexcludeZeroBuysdefault to"true", so arbitrage bots and wallets without buys are filtered out. If a bot-heavy launch returns fewer rows than you expect, pass"false"explicitly. - Labels describe activity, not intent. Sniper, insider and bundler flags come from detected activity patterns. The docs say it plainly: timing and labels help narrow a review; they do not establish a trader's intent.
- Coverage starts at indexing. The legacy endpoint states its buyers are counted since the API started recording data. Apply the same caution to tokens older than your data: the earliest rows may not be the true first buyers.
A useful pattern is to compute the cohort, not individual stories: how many of the first 50 still hold, how many exited within five minutes, and what share of supply flagged wallets still control. Those aggregates are much harder to misread than a single wallet's ROI.
Production pitfalls
- Validate mints before calling: the API expects base58, 32 to 44 characters.
- Wrap every call with a timeout and retry with backoff; the SDK client does neither, and
RateLimitError.retryAftertells you how long to wait. risk.bundlerscan be missing from the token response; usegetTokenBundlersfor the full list.- Risk wallet entries expose
wallet(with a deprecatedaddressalias in the SDK). Match onwallet ?? address. - Cache per mint for a short window. First buyers rarely change; positions do.
For where these mints come from, see streaming Pump.fun launches over WebSocket and the Pump.fun graduation API guide. The token-level score and risk factors are explained in the rug risk API guide, and ranking traders across tokens is covered in the PnL leaderboard guide.
FAQ
How many first buyers can I fetch?
limit defaults to 100 and accepts up to 200 per request. Use pagination.nextCursor for further pages; pagination.total tells you the size of the cohort after filters.
Can I sort first buyers by profit?
No. The first-buyers endpoint is chronological only. Use getPnlV2TokenTraders for ROI, PnL, size or latest-trade sorting.
What is the difference between a sniper and a first buyer?
A first buyer is any early wallet, listed in chronological order. Snipers are wallets the risk engine has flagged as such; they appear in risk.snipers on the token response and in the sniper:{token} room. Being early does not make a wallet a flagged sniper, so cross-reference the two lists instead of assuming.
Why does a wallet show zero token PnL but a large position cost?
Usually because it transferred the tokens out without selling, so there is no realized PnL and no remaining balance to mark. Check counts.sells and the wallet's transfers.
Do I need Datastream for this?
No. The report uses REST only and works with any Data API key. The live sniper, insider and bundler rooms need Datastream (Premium or higher).
References
- Find early token buyers, snipers, and bundlers
- Token traders and first buyers (PnL V2)
- Get token first buyers
- Get token bundlers
- Datastream: sniper tracking
- Datastream: insider tracking
- Monitor token risk signals over WebSocket
- @solana-tracker/data-api on npm
Companion project
The full example lives at solanatracker/examples/13-pumpfun-first-buyers-sniper-api. It picks the top 24h-volume Pump.fun token when TOKEN_MINT is empty, prints an aligned table with identity labels and hold times, counts quick flips (QUICK_FLIP_SECONDS), and switches to the live sniper, insider and bundler rooms with WATCH_RISK=true.
cp .env.example .env && npm install && npm start