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:

  1. Search tokens as the user types.
  2. Resolve exact mints when the app already knows them.
  3. Batch hydrate token rows for portfolios, routes, and watchlists.
  4. Render token logos through the cached image endpoint.
  5. Handle unknown mints, missing logos, and nullable metadata.

Base URL:

https://tokens.carbium.io

The 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 needEndpointWhy
User types a symbol, name, or mintGET /tokens?q=...&limit=...Ranked token search for selectors and import-token flows
App already has one mintGET /tokens/:mintResolve one token record directly
App has many mintsPOST /tokens/batchHydrate up to 500 token rows in one request
UI needs a token logoGET /img/:mintCached 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.

FieldUI behavior
mintStore and compare this as the durable token identity
symbolDisplay when present, but do not treat it as unique
nameDisplay as helper text; truncate long or spammy names
decimalsUse for formatting token amounts when your balance source does not already provide decimals
logo_statusShow fallback imagery when the logo is missing or broken
is_token22Show a Token-2022 hint only if your product needs users to notice
nullable enrichment fieldsTreat 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:

BehaviorDefault
Search debounce200-300ms
Search result count10-20 visible rows
Batch sizeUp to 500 mints per request
Cache token recordsIn memory for the session; backend cache for repeated server jobs
Unknown mintsShow a neutral fallback row and retry later only if the mint may be new
429 handlingStop 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.