Solana SNS RPC methods on Solana Tracker turn wallet addresses into .sol names, and names back into owners, with one JSON-RPC call each instead of chained account reads. Use them to label wallets in holder lists and leaderboards while storing the address as identity.
Solana SNS RPC methods
Each method costs 1 credit per call on the same endpoint as standard RPC. Twitter/X .twitter handles are not supported.
| Method | Params | Returns |
|---|---|---|
getPrimaryDomain | [wallet] | { wallet, domain }, domain is null when no primary is set |
getMultiplePrimaryDomains | [[wallet, ...]], up to 100 | { results: [{ wallet, domain }] } in input order |
getAllDomains | [wallet] | { wallet, domains: [...] }, including tokenized domains |
resolveSolDomain | ["name.sol"] | { domain, owner, nameAccount } or null |
These are provider extensions; a stock node returns -32601 Method not found, and you would instead derive name accounts and read them with getMultipleAccounts. If you already call Data API PnL V2 or holders endpoints, read identity.sns.domain from that response instead of making another call.
Label a page of wallets
This script validates wallets, requests primary names in batches of 100 and falls back to a shortened address (Node 20+, tsx).
# .env: full URL including ?api_key= (https://www.solanatracker.io/account/shared-rpc)
SOLANA_RPC_URL=
WALLETS=9aoUCn5J4sxhvYERCVwVnakPQxyXTHQVXe86CUJYxY8p,FMmaHPDL47V1gXsfh9WjgAT7Er3dfDvarQubTU1Jxc1r
// sns.ts: label wallets with their primary .sol names (up to 100 per call)
export {}; // ES module, so top-level await type-checks
const RPC_URL = process.env.SOLANA_RPC_URL?.trim() ?? "";
if (!RPC_URL) {
console.error("Missing SOLANA_RPC_URL. Copy the full URL from https://www.solanatracker.io/account/shared-rpc");
process.exit(1);
}
if (!URL.canParse(RPC_URL)) {
console.error("SOLANA_RPC_URL is not a valid URL"); // never print it: it contains the key
process.exit(1);
}
const BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
const input = process.env.WALLETS ?? "9aoUCn5J4sxhvYERCVwVnakPQxyXTHQVXe86CUJYxY8p,FMmaHPDL47V1gXsfh9WjgAT7Er3dfDvarQubTU1Jxc1r";
const wallets = [...new Set(input.split(",").map((w) => w.trim()).filter(Boolean))];
const invalid = wallets.filter((w) => !BASE58.test(w));
if (wallets.length === 0 || invalid.length > 0) {
console.error(`WALLETS must be comma-separated base58 addresses. Invalid: ${invalid.join(", ") || "(none given)"}`);
process.exit(1);
}
type PrimaryResult = { wallet: string; domain: string | null };
type RpcBody = {
result?: { context: { slot: number }; value: { results: PrimaryResult[] } };
error?: { code: number; message: string };
};
async function primaryDomains(batch: string[]): Promise<PrimaryResult[]> {
const res = await fetch(RPC_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getMultiplePrimaryDomains", params: [batch] }),
signal: AbortSignal.timeout(10_000),
});
if (res.status === 429) throw new Error("Rate limited (HTTP 429). Slow down or upgrade the plan.");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = (await res.json()) as RpcBody;
if (body.error) throw new Error(`RPC error ${body.error.code}: ${body.error.message}`);
if (!body.result) throw new Error("Response had no result");
return body.result.value.results;
}
const short = (w: string) => `${w.slice(0, 4)}...${w.slice(-4)}`;
try {
const rows: { wallet: string; label: string; domain: string }[] = [];
for (let i = 0; i < wallets.length; i += 100) {
// Results come back in input order; join by wallet anyway so reordering can never mislabel a row.
const byWallet = new Map((await primaryDomains(wallets.slice(i, i + 100))).map((r) => [r.wallet, r.domain]));
for (const wallet of wallets.slice(i, i + 100)) {
const domain = byWallet.get(wallet) ?? null;
rows.push({ wallet, label: domain ?? short(wallet), domain: domain ?? "(none)" });
}
}
console.table(rows);
} catch (error) {
const message = error instanceof Error && error.name === "TimeoutError" ? "Request timed out after 10s" : String(error);
console.error(message);
process.exit(1);
}
Run it with node --env-file=.env --import tsx sns.ts. getPrimaryDomain returns the same { wallet, domain } pair directly in value.
Response shape
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"context": { "slot": 423867852, "apiVersion": "3.0.0" },
"value": {
"results": [
{ "wallet": "9aoUCn5J4sxhvYERCVwVnakPQxyXTHQVXe86CUJYxY8p", "domain": "solanatracker.sol" },
{ "wallet": "FMmaHPDL47V1gXsfh9WjgAT7Er3dfDvarQubTU1Jxc1r", "domain": null }
]
}
}
}
domain is null when no primary name is set or when the favourite domain is no longer owned by the wallet. getAllDomains returns an empty domains array for a wallet with none, and resolveSolDomain returns value: null for an unknown name. It handles root names and one level of subdomain.
Pitfalls
- Storing names as identity. Names transfer. Store the address, render the name.
- Treating
nullas an error. It is a normal result; show the shortened address. - Sending money to a cached name. Before any transfer, call
resolveSolDomainat that moment and show the resolved owner. - One call per row. Batch the visible page; cache labels briefly,
nullincluded. - Checking only HTTP status. A 200 can carry a JSON-RPC
error.
For full identity enrichment beyond SNS, see the wallet identity guide. The PnL leaderboard guide shows SNS labels on ranked wallets. Both the Solana RPC endpoint and the Solana Data API return these names.
FAQ
How many wallets can one call resolve?
getMultiplePrimaryDomains accepts up to 100 wallets and returns results in input order. Split longer lists.
What does a null domain mean?
The wallet has no primary name, or its favourite domain is no longer owned. The address is still valid.
Do these methods work on any Solana RPC provider?
No. They are Solana Tracker extensions; other endpoints return Method not found.
How much does a lookup cost?
One credit per call, batch calls included.