web3icons

Token lists and logoURI: where wallets get token logos

How the token list standard and its logoURI field supply token logos to wallets, how to resolve IPFS links, and how to cache and validate remote images.

5 min read

When a wallet or a swap interface shows the right logo next to a token, the image usually came from a token list: a JSON file, published by someone, that maps token contracts to names, symbols, decimals and a logoURI. Your app reads the list, fetches the image at that URI, and draws it.

That last step means displaying an image from a URL that someone else controls. This guide covers the format, the two kinds of URLs you will meet, and how to cache and validate logos so a list can't break or mislead your interface.

What a token list looks like

The format is defined by the token list standard at tokenlists.org, as a JSON schema. A minimal list with one token:

{
  "name": "Example List",
  "timestamp": "2026-11-01T00:00:00.000Z",
  "version": { "major": 1, "minor": 4, "patch": 0 },
  "logoURI": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
  "tokens": [
    {
      "chainId": 1,
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "name": "USD Coin",
      "symbol": "USDC",
      "decimals": 6,
      "logoURI": "https://tokens.example.com/logos/usdc.svg"
    }
  ]
}

Each token entry belongs to one chain. USDC on Ethereum (chain ID 1) and USDC on Base (chain ID 8453) are separate entries with different contract addresses, even if both point at the same logo file. The decimals value matters too: USDC uses 6 on Ethereum, not the 18 that many ERC-20 tokens use, and a list with the wrong number makes every balance wrong by a factor of a trillion.

Lists are versioned. The standard's guidance is to bump the major version when tokens are removed, the minor version when tokens are added, and the patch version when details of existing tokens change, such as a new logo URL. If you cache lists, the version tells you when to refresh derived data like logo thumbnails.

The logoURI field

logoURI is optional, both on each token and on the list itself. When it is missing, your app needs another source or a fallback. When it is present, it is just a URI. The schema doesn't fix the image format or dimensions, so in practice you will see PNG, SVG, JPEG and WebP files, from 32 px icons to 2,000 px exports, with or without transparent backgrounds and padding.

So treat logoURI as a pointer to raw material. Before it reaches your interface, it should be resolved, fetched, checked and normalized.

HTTPS and IPFS URLs

Two schemes cover nearly every list.

https:// URLs point at a web server: a project's own site, a GitHub-hosted asset, a CDN. They load directly, but the file behind them can change at any time. A logo that was correct when the list was reviewed may not be correct next month.

ipfs:// URIs reference content on IPFS by its content identifier (CID). The CID is derived from the file's content, so the same CID always means the same bytes. That is a useful property for logos: what the list maintainer reviewed is what your users will see. Browsers can't load ipfs:// directly, so you translate it to an HTTP gateway URL:

const IPFS_GATEWAY = 'https://ipfs.example.com'

export function resolveLogoURI(uri) {
  if (typeof uri === 'string' && uri.startsWith('ipfs://')) {
    const path = uri.slice('ipfs://'.length).replace(/^ipfs\//, '')
    return `${IPFS_GATEWAY}/ipfs/${path}`
  }
  if (typeof uri === 'string' && uri.startsWith('https://')) {
    return uri
  }
  return null
}

The replace handles an older ipfs://ipfs/<CID> form that still appears in some lists. Everything that isn't ipfs:// or https://, including http:, data: and javascript: URIs, resolves to null and gets a fallback icon.

Public gateways are shared infrastructure. They can be slow, rate-limit busy apps or go offline, so a wallet that loads hundreds of logos from one public gateway will see failures. Running your own gateway, or fetching each file once and storing it yourself, is more dependable.

Cache logos on your own infrastructure

Loading every logoURI directly in the user's browser has three problems. Third-party hosts fail and slow down your lists. They can change the file after review. And every request tells the host the user's IP address and which logos they loaded, which usually means which tokens they hold. For a wallet, that is a real privacy leak.

A small logo proxy solves all three:

  1. Fetch on the server, once per URI, with a timeout of a few seconds.
  2. Check the response. Accept only image content types (image/png, image/jpeg, image/webp, image/svg+xml), and reject anything over a sensible limit; a token logo rarely needs more than 100 to 200 KB.
  3. Normalize raster images. Decode and re-encode them to fixed sizes, for example 64 and 128 px PNG or WebP. Re-encoding strips metadata and anything that isn't pixels, and gives every logo the same dimensions.
  4. Treat SVG as untrusted code. Either rasterize it, or sanitize it with an allowlist of elements and attributes. Serve SVG files from a separate domain or with a restrictive Content-Security-Policy, because opening an SVG URL directly in a browser runs any script it contains.
  5. Cache by URI and list version. Serve results with long cache lifetimes, and remember failures too, so a dead URL isn't refetched on every request.

In the browser, always display these logos with <img>, never by inlining the SVG markup. Images loaded through <img> can't run scripts or reach your page's DOM. The SVG security guide explains the difference, and the caching guide covers the HTTP headers.

A logo is a claim, not a proof

A token list is someone's opinion about which contracts are which. Lists from well-known maintainers are reviewed, but anyone can publish one, and a scam token can carry a perfect copy of a real logo. The logo shows what the list maintainer believes; it doesn't verify the contract.

A few practices follow from that:

  • Let users see which list a token came from, and treat tokens from user-added lists or manual imports as unverified until they choose otherwise.
  • Match tokens by chain ID and address, never by symbol. Symbols repeat freely.
  • When several lists disagree about a token's logo, prefer the list you trust most, and don't merge logos from a list you don't trust into tokens from one you do.
  • The same caution applies outside lists. With wallet_watchAsset (EIP-747), a dApp can suggest a token to a wallet together with an image URL. Wallets should handle that image with the same proxying and validation as a list logo.

When a logo is missing, fails validation, or comes from a source you don't trust, show a neutral fallback instead; the fallback icons guide shows how to build one. For the broader problem of tokens designed to look like others, see fake tokens and spoofed logos.

More in Using crypto logos

All guides