Build a Token Selector With Carbium Data
Build a production-friendly Solana token selector with Carbium Data: debounced search, batch mint hydration, cached logos, nullable metadata, and rate-limit-safe client behavior.
Build a Token Selector With Carbium Data
A token selector has one job: help users find the mint they mean without making your app slow, noisy, or wrong.
Carbium Data gives you the pieces for that workflow through the public Token Index API at https://tokens.carbium.io. Use it for search, known-mint hydration, cached token logos, and display metadata. Keep swap pricing, route choice, and executable transaction truth in the Swap API.
What you are building
This guide builds the Data side of a token picker:
- Search tokens as the user types.
- Resolve exact mints when the app already knows them.
- Batch hydrate token rows for portfolios, routes, and watchlists.
- Render token logos through the cached image endpoint.
- Handle unknown mints, missing logos, and nullable metadata.
Base URL:
https://tokens.carbium.ioThe current public surface does not require an API key. It is rate-limited by route, so production apps should debounce search, cache common records, and batch known mints.
Request map
| UI need | Endpoint | Why |
|---|---|---|
| User types a symbol, name, or mint | GET /tokens?q=...&limit=... | Ranked token search for selectors and import-token flows |
| App already has one mint | GET /tokens/:mint | Resolve one token record directly |
| App has many mints | POST /tokens/batch | Hydrate up to 500 token rows in one request |
| UI needs a token logo | GET /img/:mint | Cached logo proxy with app-side fallback |
Use Token API Calls for the full request and response reference.
Search with debounce
For browser selectors, avoid calling the API on every keypress. Wait briefly after typing stops, then cancel stale requests when the user continues typing.
const BASE = "https://tokens.carbium.io";
type TokenRow = {
mint: string;
symbol: string | null;
name: string | null;
decimals: number | null;
logo_status?: "ok" | "unchecked" | "broken" | "missing";
is_token22?: boolean;
};
let searchController: AbortController | null = null;
export async function searchTokens(query: string): Promise<TokenRow[]> {
const q = query.trim();
if (q.length < 2) return [];
searchController?.abort();
searchController = new AbortController();
const url = new URL("/tokens", BASE);
url.searchParams.set("q", q);
url.searchParams.set("limit", "20");
const response = await fetch(url, { signal: searchController.signal });
if (response.status === 429) {
throw new Error("Token search is rate-limited. Slow down and retry.");
}
if (!response.ok) {
throw new Error("Token search failed with HTTP " + response.status);
}
const data = await response.json();
return data.items ?? [];
}A practical selector policy is 200-300ms debounce, limit=20, and no request until the query has enough characters to be useful.
Resolve selected and known mints
Once a user picks a token, store the mint address as the durable value. Symbols and names can collide, change, or be incomplete.
export async function getToken(mint: string): Promise<TokenRow | null> {
const response = await fetch(BASE + "/tokens/" + encodeURIComponent(mint));
if (response.status === 404) return null;
if (response.status === 429) {
throw new Error("Single-mint lookup is rate-limited. Retry later.");
}
if (!response.ok) {
throw new Error("Token lookup failed with HTTP " + response.status);
}
return response.json();
}Use this for exact mint imports, token-detail drawers, and server-side validation when a form submits a selected mint.
Batch hydrate token rows
Portfolios, route previews, watchlists, and admin tables usually start with a list of mints. Do not loop over GET /tokens/:mint for every row. Use POST /tokens/batch.
export async function hydrateTokens(mints: string[]) {
const uniqueMints = [...new Set(mints)].slice(0, 500);
const response = await fetch(BASE + "/tokens/batch", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ mints: uniqueMints }),
});
if (response.status === 429) {
throw new Error("Batch token lookup is rate-limited. Back off and retry.");
}
if (!response.ok) {
throw new Error("Batch token lookup failed with HTTP " + response.status);
}
const data = await response.json();
return {
byMint: new Map((data.items ?? []).map((token: TokenRow) => [token.mint, token])),
unknown: data.unknown ?? [],
};
}Treat unknown as a normal result. It can mean the mint is not indexed yet, the address is not a token mint, or the user pasted the wrong value.
Render logos safely
Use the image endpoint when you want a stable Carbium URL for token logos:
function TokenLogo({ mint, symbol }: { mint: string; symbol?: string | null }) {
const fallback = "/token-fallback.svg";
return (
<img
src={"https://tokens.carbium.io/img/" + encodeURIComponent(mint)}
alt={symbol ? symbol + " logo" : "Token logo"}
width={32}
height={32}
loading="lazy"
onError={(event) => {
event.currentTarget.src = fallback;
}}
/>
);
}Even with a cached proxy, keep a local fallback icon. Solana token metadata is uneven, and logo states can be ok, unchecked, broken, or missing.
Display rules that prevent bad UX
Use mint as the stable identifier, then layer display fields on top.
| Field | UI behavior |
|---|---|
| mint | Store and compare this as the durable token identity |
| symbol | Display when present, but do not treat it as unique |
| name | Display as helper text; truncate long or spammy names |
| decimals | Use for formatting token amounts when your balance source does not already provide decimals |
| logo_status | Show fallback imagery when the logo is missing or broken |
| is_token22 | Show a Token-2022 hint only if your product needs users to notice |
| nullable enrichment fields | Treat null as normal, not as an exception |
For swap screens, use Carbium Data to make token rows readable. Use Swap API quote responses for route, price, output, and transaction decisions.
Rate-limit-safe client behavior
The public Token Index API is free and route-limited. Build the selector as if every repeated request matters.
Recommended defaults:
| Behavior | Default |
|---|---|
| Search debounce | 200-300ms |
| Search result count | 10-20 visible rows |
| Batch size | Up to 500 mints per request |
| Cache token records | In memory for the session; backend cache for repeated server jobs |
| Unknown mints | Show a neutral fallback row and retry later only if the mint may be new |
| 429 handling | Stop immediate retries, respect Retry-After when present, and back off |
Read Data Rate Limits before shipping the selector into a high-traffic app.
Minimal production checklist
- Search waits until the query is useful and debounced.
- Previous search requests are cancelled when newer input arrives.
- Selected token state stores the mint, not only the symbol.
- Portfolio and route tables use POST /tokens/batch.
- Logo rendering has a local fallback.
- Nullable metadata fields do not break the UI.
- 429 responses trigger backoff instead of tight retries.
- Swap execution still uses Swap API route and transaction responses as the source of truth.
Use Carbium Data for token identity and display. Use Carbium RPC for chain reads, Swap API for executable swap flows, and gRPC when you need real-time Solana stream processing.
Updated 3 days ago
