The Solana wallet identity API is the identity object that Solana Tracker attaches to wallet addresses: ask for it with enrich: 'identity' on token trade history, getTokenHolders(mint, 'identity') on the top 100 holders, or { enriched: true } on live Datastream rooms. Each identity carries a display name, a primary badge in type, the full tags array (KOL, bot, exchange, developer, pool, SNS and more) and role details such as the pool program or a .sol domain. Unknown wallets come back as identity: null, which is a normal answer, not an error.
This guide is about who a wallet is. What it holds is covered by the Solana wallet portfolio API, how much one wallet made is the Solana wallet PnL API, and ranking wallets against each other is the Solana PnL leaderboard API. Identity is the layer you put on top of all three.
Where the Solana wallet identity API appears
You do not call a separate identity endpoint. Identity is enrichment on responses you already use from the Solana Data API, so you get labels in the same request as the trades or holders you are rendering.
| Surface | How to request it | Shape | Notes |
|---|---|---|---|
| Token trade history | getTokenTradeHistory(mint, { enrich: 'identity' }) | identity on each trade | limit 1 to 500 |
| Top 100 holders | getTokenHolders(mint, 'identity') | identity on each account | Add walletPnl or use 'all' to include PnL per holder |
| PnL v2 wallet, token traders, first buyers, leaderboards, batch | Returned by default | Top-level or per-row identity | Present when a label is known |
| KOL and whale trade feeds | getKolTrades(), getWhaleTrades() | identity per trade when known | KOL trades come from the tracked roster |
| Live swaps and liquidity | subscribe.tx.token(mint, { enriched: true }) and liquidity rooms | identity plus optional identityStatus | Joins the :enriched variant of the room; Premium plan or higher |
The REST and WebSocket options are separate contracts. enrich: 'identity' on a REST call does nothing to a Datastream subscription, and { enriched: true } on a room does nothing to REST. Request enrichment on each surface that needs it, and only there: a small autocomplete or a price ticker has no use for wallet labels.
Holder enrichment has more options than trades. Pass 'identity' when the table only needs names and roles, 'walletPnl' when it only needs profit columns, 'identity,walletPnl' or 'all' when it shows both. Larger enrichments mean larger payloads, so match the request to what the screen renders.
Identity fields and tags
The SDK types the object as PnlV2Identity. Every field is optional, so guard each read.
| Field | Type | What it holds |
|---|---|---|
name | string | Best display label: a KOL name, a promoted exchange or hacker label, or the .sol domain when nothing curated exists |
type | string | One primary tag, meant for a single badge |
tags | string[] | Every tag that applies; always read it as an array |
twitter, avatar | string | KOL profile data when available |
sns | { domain } | Primary .sol domain |
bot, exchange, hacker, spamDusting | object | Name or label for curated wallets |
pool | object | program, poolAddress, tokenA, tokenB for pool accounts |
developer | object | token, via, pools, creationTx and related creation context |
platforms | string[] | Trading frontends seen for the wallet |
The documented tag set covers people, infrastructure and token context:
| Tag | Meaning |
|---|---|
kol | Tracked KOL wallet with a public profile |
bot | Curated named bot, such as Mayhem Bot |
potential_bot | Heuristic bot or arbitrage-like signal; softer than bot |
arbitrage | Confirmed MEV or arbitrage wallet |
exchange | Known centralized exchange hot wallet |
hacker, spam_dusting | Curated exploit, scam or dusting wallet |
developer | Creator, pool deployer or mint authority for the requested token |
pool | Pool vault, authority, LP mint or bonding curve account |
axiom, bloom, photon | Wallet activity associated with that trading platform |
sns | Wallet has a primary .sol domain |
Two rules matter most. First, developer and pool are resolved against the mint in the request, so the same wallet can be a developer on one token and an ordinary trader on another. Second, a wallet can carry several tags at once, for example kol and sns. Use type when you have room for one badge and tags for filters or multi-badge rows.
How labels look in responses
These are trimmed from the documented holder and SNS examples. A pump.fun pool account returned by getTokenHolders has no name, only a role:
{
"wallet": "HYLHTXn74S378oYQHmoJa9yqrfr3DCd8TZtnFCNvySC5",
"identity": {
"pool": { "program": "pumpfun", "poolAddress": "HYLHTXn74S378oYQHmoJa9yqrfr3DCd8TZtnFCNvySC5" },
"type": "pool",
"tags": ["pool"]
}
}
A curated bot has a name promoted from bot.name:
{
"wallet": "BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s",
"identity": {
"bot": { "name": "Mayhem Bot", "avatar": "https://kol-avatar.solanatracker.io/..." },
"name": "Mayhem Bot",
"type": "bot",
"tags": ["bot"]
}
}
A wallet with only a primary domain gets the domain as its name and type: "sns". When a curated label exists, name keeps that label and the domain moves to sns.domain as secondary context:
{ "identity": { "name": "solanatracker.sol", "sns": { "domain": "solanatracker.sol" }, "tags": ["sns"], "type": "sns" } }
{ "identity": { "name": "SomeKolName", "sns": { "domain": "solanatracker.sol" }, "tags": ["kol", "sns"], "type": "kol" } }
And a wallet with nothing known is simply "identity": null. Unlabeled wallets are common, so treat the null path as a main path of your UI, not an edge case.
Label trades and holders in TypeScript
The snippet below fetches recent swaps with identity, falls back cleanly when a wallet is unknown, and then labels the top holders. It uses @solana-tracker/data-api 0.5.0 and reads the key from the environment.
# .env.example
ST_API_KEY=your_data_api_key
TOKEN_MINT=6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
import { Client, DataApiError, RateLimitError, type PnlV2Identity } from '@solana-tracker/data-api';
const apiKey = process.env.ST_API_KEY;
if (!apiKey) throw new Error('Set ST_API_KEY in .env');
const client = new Client({ apiKey });
const mint = process.env.TOKEN_MINT ?? '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN';
const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`;
const clean = (s: string) => s.replace(/[\u0000-\u001f\u007f]/g, '').trim();
// Name first, then .sol domain, then curated sub-labels, then the address.
function displayName(id: PnlV2Identity | null | undefined, wallet: string): string {
const label = id?.name || id?.sns?.domain || id?.bot?.name || id?.exchange?.name;
return label ? clean(label) : short(wallet);
}
try {
const history = await client.getTokenTradeHistory(mint, {
enrich: 'identity',
limit: 100,
sortDirection: 'DESC',
});
for (const t of history.trades.slice(0, 20)) {
const badge = t.identity?.type ?? (t.identity ? 'labeled' : '-');
console.log(t.type.padEnd(4), displayName(t.identity, t.wallet).padEnd(20), badge.padEnd(14), (t.identity?.tags ?? []).join(','));
}
const labeled = history.trades.filter((t) => t.identity).length;
console.log(`Labeled ${labeled}/${history.trades.length} trades`);
const holders = await client.getTokenHolders(mint, 'identity');
for (const h of holders.accounts.slice(0, 10)) {
const role = h.identity?.pool ? `pool ${h.identity.pool.program ?? ''}` : h.identity?.developer ? 'developer' : '';
console.log(short(h.wallet), displayName(h.identity, h.wallet).padEnd(20), role.padEnd(16), `${h.percentage.toFixed(2)}%`);
}
} catch (err) {
if (err instanceof RateLimitError) console.error(`Rate limited, retry after ${err.retryAfter ?? '?'}s`);
else if (err instanceof DataApiError) console.error(`Data API error ${err.status}: ${err.message}`);
else throw err;
process.exitCode = 1;
}
The fallback order is deliberate. name is the API's own choice of best label. sns.domain covers wallets that only have a domain. bot.name and exchange.name cover objects where a sub-label exists but was not promoted. The shortened address is the final answer for everything else, and it keeps every row clickable through to the wallet.
Twitter values are not always normalized. A handle can arrive with a leading @, and occasionally with two, so strip every leading @ and add one back before rendering. Treat every string in identity as untrusted text: render it as text, never as HTML, and validate avatar and profile URLs before linking them. A label is a hint, and a wallet name that looks like a verified badge must not be able to impersonate your own UI chrome.
Enriched live trades over Datastream
For a live tape, pass { enriched: true } when subscribing. The SDK joins the :enriched room and types each event as EnrichedTokenTransaction, which adds identity and an optional identityStatus. Enriched rooms need a Premium, Business or Enterprise plan.
# .env.example
ST_DATASTREAM_KEY=your_datastream_key
TOKEN_MINT=6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
import { Datastream } from '@solana-tracker/data-api';
const key = process.env.ST_DATASTREAM_KEY;
if (!key) throw new Error('Set ST_DATASTREAM_KEY in .env');
const mint = process.env.TOKEN_MINT ?? '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN';
// The URL contains your key: never log it.
const ds = new Datastream({
wsUrl: key.startsWith('wss://') ? key : `wss://datastream.solanatracker.io/${key}`,
autoReconnect: true,
});
ds.on('connected', () => console.log('Datastream connected'));
ds.on('error', () => console.error('Datastream error (check key and plan)'));
const sub = ds.subscribe.tx.token(mint, { enriched: true }).on((tx) => {
const who = tx.identity?.name ?? tx.identity?.sns?.domain ?? `${tx.wallet.slice(0, 4)}…${tx.wallet.slice(-4)}`;
const note = tx.identityStatus === 'partial' ? ' (label lookup incomplete)' : '';
console.log(`${tx.type} ${who} $${tx.volume.toFixed(2)}${note}`);
});
await ds.connect();
process.once('SIGINT', () => {
sub.unsubscribe();
ds.disconnect();
process.exit(0);
});
identityStatus: 'partial' means the lookup did not finish for that event, and no correction will follow for it. Render the address fallback and let a later event, or a REST refresh, supply the label. Enriched events can also arrive later or in a different order than the plain room, so sort by time and deduplicate by tx rather than trusting arrival order.
Production pitfalls
Labels are current, not historical. Enrichment on a trade from last month returns the wallet's label today. If you backtest a strategy that follows "KOL buys", filtering old trades by today's kol tag adds hindsight: wallets that became KOLs because they did well are overrepresented. When an analysis must be reproducible, store the identity snapshot with the time you observed it.
Join on the address, never the name. Names change, can collide and can be empty. Keep wallet as the key in your database and caches, and treat identity as a column you can refresh at any time without touching amounts, signatures or deduplication keys.
Token-scoped roles do not travel. A developer or pool label from getTokenHolders for one mint says nothing about another mint. Do not cache those roles globally per wallet. Cache them per mint, or not at all.
Separate infrastructure from people. Pool, bonding curve and exchange accounts often sit at the top of a holder list. Exclude pool and exchange accounts before you compute concentration, and treat a developer tag as an observed role, not proof of insider trading.
A label is not a verdict. A kol tag says who the wallet is, not that it trades well. Check performance with the PnL leaderboard API or the wallet's own PnL before weighting its trades.
Resolve domains only when you need to. If identity.sns.domain is present, use it. For screens that list addresses without enrichment, the SNS RPC methods on Solana RPC resolve primary domains directly; batch the visible addresses and cache with an expiry.
FAQ
Is there a single endpoint to look up one wallet's identity?
Identity is enrichment, not a standalone route. For one wallet, the PnL v2 wallet summary returns a top-level identity object alongside its PnL; for lists, use the trade history and holder enrichment shown above.
Why is identity null for most wallets?
A wallet with no curated label, no .sol domain and no token role has nothing to return. null means nothing is known, so render the shortened address and keep the row.
What is the difference between type and tags?
type is the single primary tag for a one-badge UI. tags lists every label that applies, so a wallet can be both kol and sns. Filter on tags.
Does a wallet's label change over time?
Yes. Enrichment returns current, cached labels on both REST and live rooms. Store a snapshot if you need the label as it was at a specific time.
What does identityStatus: 'partial' mean?
The identity lookup for that live event was incomplete. No follow-up correction is sent for the event, so fall back to the address and refresh labels later.
Can I filter a leaderboard by trading platform?
Yes. The top traders leaderboard, token traders and first buyers accept platform=axiom, bloom or photon. axiom-flash is accepted and normalized to axiom in identity tags.
References
- Identify Solana wallets with tags and labels
- Show Solana wallet names with SNS identity
- Get token trades
- Get token holders (top 100)
- Whale and KOL trades guide
- Live transactions guide
- @solana-tracker/data-api on npm
Companion project
The solana-wallet-identity-enrichment example labels recent swaps and top holders for a token, reports how much of the flow comes from labeled wallets, and breaks down holders by token-scoped role.
cp .env.example .env && npm install && npm start