# How to map a ticker to exactly one coin

A ticker names a dozen tokens. Resolving one to a single asset across CoinGecko, CoinMarketCap, CCXT and the chains, and what a rename does to the table.

*https://cryptomarkets.tools/how-to/map-a-ticker-to-one-coin · next to Crypto Market Data APIs*

**Answer:** Never key on the ticker. Resolve it once to a vendor ID, a CoinGecko string or a CoinMarketCap number, and store the chain and contract address beside it, because that pair is the only identifier two vendors will agree on. Exchange pair names map through CCXT's market table. Then re-check the table on a schedule: tickers are shared, renamed and bridged, and a mapping built once goes wrong without raising an error.

## Approaches

*In the author’s order. Paid placement does not affect it.*

1. [CoinGecko API](https://cryptomarkets.tools/tools/coingecko-api.md) — One keyless call returns every coin ID with its contract on every chain, and a second resolves a chain and address back to the ID.
2. [CoinMarketCap API](https://cryptomarkets.tools/tools/coinmarketcap-api.md) — The numeric ID map, keyless, with inactive coins on request; the metadata endpoint takes a contract address and returns the ID it belongs to.
3. [DefiLlama](https://cryptomarkets.tools/tools/defillama.md) — Prices keyed by chain and address, or by CoinGecko ID, and a chains table that joins CoinGecko, CoinMarketCap and EVM chain IDs.
4. [CCXT](https://cryptomarkets.tools/tools/ccxt.md) — Loads each venue's market list and maps its own pair strings to one unified symbol, with the currency renames overridable in code.
5. [CoinAPI Market Data API](https://cryptomarkets.tools/tools/coinapi.md) — One paid symbol scheme over its venues, with a per-exchange map back to the venue's own codes and chain addresses on each asset.

## The short way

Resolve the ticker once, against a vendor's ID map, and never send it again. Both of the large
aggregators publish one, and both answer without a key.

```text
GET https://api.coingecko.com/api/v3/coins/list?include_platform=true
GET https://pro-api.coinmarketcap.com/public-api/v1/cryptocurrency/map?symbol=POL,MATIC
```

The first returns every active coin CoinGecko tracks, in one unpaginated response, as a string
`id`, a `symbol`, a `name`, and with `include_platform` a map of every chain the token lives on to
its contract address there. The second returns CoinMarketCap's numeric `id` for every coin using
each symbol, with a `platform` object and an `is_active` flag. Build two indexes from the first,
one from ticker to candidate IDs and one from chain and address to exactly one ID:

```python
import json
import urllib.request

URL = "https://api.coingecko.com/api/v3/coins/list?include_platform=true"
with urllib.request.urlopen(URL, timeout=60) as response:
    coins = json.load(response)

by_symbol, by_contract = {}, {}
for coin in coins:
    by_symbol.setdefault(coin["symbol"].lower(), []).append(coin["id"])
    for chain, address in (coin.get("platforms") or {}).items():
        if not chain or not address:
            continue  # a few entries arrive with an empty chain or address
        if address.startswith("0x"):
            address = address.lower()  # hex: the case carries no identity
        by_contract[(chain, address)] = coin["id"]

print(by_symbol["pol"])
print(by_contract[("ethereum", "0x455e53cbb86018ac2b8092fdcd39d8444affc3f6")])
```

Run on 26 September 2026, the first line printed four IDs and the second printed one:
`polygon-ecosystem-token`. That asymmetry is the whole page. A ticker is a search term with
several answers; a chain and a contract address is a key with one. Across the 21,602 entries
that call returned, 2,505 tickers were shared by more than one coin, and no chain-and-address
pair was shared by two.

So store three things per asset: the vendor ID you price from, the chain, and the address. The
ticker goes in a display column and nowhere else.

## What the options are

**The two identifier layers.** The [CoinGecko API](https://cryptomarkets.tools/tools/coingecko-api) keys on strings —
`bitcoin`, `polygon-ecosystem-token` — and its coin list is the cheapest complete dump in the
market: one call, no pagination, refreshed every 30 minutes on the keyless and Demo tiers and
every five on paid ones. Going the other way, `/coins/{platform}/contract/{address}` returns the
coin for a chain and an address, where the platform is one of the IDs from `/asset_platforms`.
The [CoinMarketCap API](https://cryptomarkets.tools/tools/coinmarketcap-api) keys on integers, and its documentation
recommends the CMC ID over the symbol "to securely identify cryptocurrencies with our other
endpoints and in your own application logic". Its map takes `listing_status=inactive` and
`untracked` as well as the default `active`, and `/v2/cryptocurrency/info` accepts an `address`
parameter, so a contract resolves to an ID there too. Both run on the keyless public API.

**A price lookup that takes the address itself.** [DefiLlama](https://cryptomarkets.tools/tools/defillama)'s coins API is
keyed by `{chain}:{address}`, or by `coingecko:{id}` for assets with no contract — so a table
that already holds chain and address needs no vendor ID at all to be priced. Its `/v2/chains`
endpoint is the other useful half: per chain it returns the EVM chain ID and, where the chain has
a token of its own, that token's CoinGecko and CoinMarketCap IDs, which is a ready-made join
between the three schemes for the one asset per chain that has no contract.

**The venue's own pair names.** Exchanges do not use tickers either; they use market IDs. The
[CCXT](https://cryptomarkets.tools/tools/ccxt) manual lists what BTC/USD is called on various exchanges — `btcusd`,
`XBTUSD`, `tBTCUSD`, `XXBTZUSD`, and `42` — and `load_markets()` builds the table from each
venue's live listing, keyed both ways: `exchange.markets[symbol]["id"]` and
`exchange.markets_by_id[id][0]`. Unified symbols are `BASE/QUOTE` for spot, `BASE/QUOTE:SETTLE`
for a perpetual; futures add an expiry, and options an expiry, a strike and put or call. The currency
renames it applies are in `exchange.commonCurrencies`, which you can override before the markets
load. [How to place an order on several exchanges](https://cryptomarkets.tools/how-to/place-an-order-on-several-exchanges)
is the same table from the trading side.

**One scheme, bought.** [CoinAPI](https://cryptomarkets.tools/tools/coinapi) names every market
`EXCHANGE_TYPE_BASE_QUOTE` — `BINANCE_SPOT_BTC_USDT` — and `/v1/symbols/map/{exchange_id}`
returns, per venue, its own `symbol_id` beside the exchange's `symbol_id_exchange`, and its
`asset_id_base` beside the exchange's `asset_id_base_exchange`. Asset records carry a
`chain_addresses` list. It is the only option here with no free way in: the entry point is a
metered account with a card on file.

## Where this breaks

**Every lookup that returns one answer made a choice.** Ask for a symbol and get a single
record, and somebody ranked the candidates for you. CoinMarketCap's v1 metadata endpoint returns
"the highest ranked coin using that symbol"; v2 returns an array, "due to the fact that a symbol
is not unique". CoinGecko's price endpoint accepts `symbols` and, with `include_tokens=top`,
returns "top-ranked tokens by market cap or volume"; `all` returns every match instead. CCXT
settles a clash between coins that share a code on different exchanges by market capitalisation, so
`HOT/USD` is Holo and the other token trades as `Hydro Protocol/USD`. Each rule is reasonable and
each is a ranking — which means the answer to "which BTC" can change without any of the coins
changing, if another one climbs past it.

**The same token on six chains is six addresses, and the vendors disagree about which of them
are the same asset.** CoinMarketCap's USDC is one ID, 3408, whose metadata lists 95 contract
addresses. CoinGecko's `usd-coin` lists 36 platforms, and 62 IDs in the same list carry the
ticker `usdc` — most of them bridged copies with IDs of their own. Neither is wrong: a token
issued on a chain and a token bridged onto it are different claims on the same dollar, and each
vendor draws the line where its methodology does.
[How to reconcile a token's supply and market cap](https://cryptomarkets.tools/how-to/reconcile-supply-and-market-cap) is
what that line does to a supply figure. For a mapping, it means a CoinGecko ID and a
CoinMarketCap ID do not always name the same set of contracts, and a join between the two
through the ticker will pair them wrongly.

**The address alone is not a key either.** The same string can be deployed on several chains. In
the CoinGecko list, 1,808 addresses appear under more than one platform, and `wrapped-bitcoin`
lists `0x0555e30da8f98308edb960aa94c0db47230d2b9c` on Base, Sonic and Berachain alike. The key is
the pair, and the chain half has no shared spelling: Polygon PoS is EVM chain `137`,
`polygon-pos` on CoinGecko, `polygon_pos` on GeckoTerminal, `polygon` in DefiLlama's coin keys,
`POLYGON` in CoinAPI's chain IDs and `MATIC` in the CCXT manual's table of unified networks.
Store the EVM chain ID where there is one — CoinGecko's `/asset_platforms` and DefiLlama's
`/v2/chains` both return it — and map names to it, not to each other.
[GeckoTerminal](https://cryptomarkets.tools/tools/geckoterminal) does part of this for you by returning a
`coingecko_asset_platform_id` on every network.

**A rename is a new ID, and the old one does not forward.** Polygon replaced MATIC with POL as the
native token of Polygon PoS on 4 September 2024. On 26 September 2026, CoinGecko still listed
`matic-network` as an active entry named "MATIC (migrated to POL)", with no contracts, beside
`polygon-ecosystem-token`, "POL (ex-MATIC)". CoinMarketCap's MATIC, ID 3890, was inactive; its
POL is ID 28321, with history from 25 October 2023. A series keyed to the old ID ends at the
rename rather than continuing under the new ticker, and a job that treats an empty response as
"no trades today" will not notice. Labels lag further than IDs: DefiLlama's price for
`polygon:0x0000000000000000000000000000000000001010` is POL's price under the symbol `matic`.

**Delisted is not deleted, and the free tier cannot see it.** CoinGecko's coin list returns
inactive coins only with `status=inactive`, which needs the Analyst plan or above; a stored ID for
a coin that stopped trading simply drops out of the free list. CoinMarketCap's map returns
inactive coins on the keyless tier, and asking it for a symbol returns them unasked — the
`symbol=POL,MATIC` request above came back with five records, three of them inactive. Keep
inactive IDs in your table and flag them; deleting them is how a backtest loses the coins that
went to zero.

**Checksummed and lowered addresses are the same address.** ERC-55 puts a checksum in the
capitalisation of hex letters, and 211 EVM addresses in CoinGecko's list arrive in mixed case
while CoinMarketCap returns USDC on zkSync Era as `0x1d17CBcF…`. Compare hex addresses lower-cased.
Do not lower-case everything: a Solana mint address is base58, where upper and lower case are
different characters.

**Wrapped is a different asset with a different risk.** WBTC is not BTC, and both aggregators give it
its own ID — `wrapped-bitcoin` beside `bitcoin` on CoinGecko, where Bitcoin has no contract at
all, and 3717 beside 1 on CoinMarketCap. The price
tracks until the custodian or the bridge behind it fails, which is exactly when the difference
matters. Map the ticker a user typed to the asset they meant, and keep the wrapper as its own row.

## If you outgrow this

If the problem is **that the table drifts**, rebuild it on a schedule rather than patching it:
pull the full list from the vendor you price from, diff it against yesterday's, and review what
appeared, disappeared or changed symbol before anything downstream reads it. The CoinGecko list
is one call and CoinMarketCap's map is keyless, so the cost of doing this daily is a few requests.

If the problem is **exchange coverage** — the same asset across many order books rather than one
aggregate — CCXT's market tables are free and rebuilt every time they load, and CoinAPI sells the
same mapping maintained for you across its venues, with the archive behind it.
[Market data APIs](https://cryptomarkets.tools/categories/market-data-apis) is the rest of that shelf.

If the problem is **a migration off an aggregator that did the mapping for you**, that is most of
the work, and [the CryptoCompare page](https://cryptomarkets.tools/alternatives/cryptocompare) says so in its migration notes:
its `fsym` was a ticker, and every replacement wants an ID.

And if the problem is **a token no aggregator lists**, there is no vendor ID to resolve to. Key it
by chain and address from the start, price it from the pool it trades in, and read
[how to get the price of a DEX pair](https://cryptomarkets.tools/how-to/get-a-dex-pair-price) before trusting that number.

## FAQ

### Why does a ticker return more than one coin?

Because nobody allocates tickers. Any token contract can call itself BTC, and bridged copies of a real asset usually keep the original's ticker on purpose. On 26 September 2026 CoinGecko's coin list held 21,602 entries, and 2,505 tickers were shared by more than one of them; `btc` alone belonged to 12 IDs. CoinMarketCap's own metadata documentation says it outright — a symbol is not unique.

### Should I store the CoinGecko ID or the CoinMarketCap ID?

Whichever vendor you actually pull prices from, plus the chain and contract address for any token that has one. The vendor ID is what the vendor's endpoints accept; the chain and address pair is what lets you move to another vendor without re-resolving every row by hand. DefiLlama's chains table already carries both vendors' IDs for each chain's own token, which is a useful check on your own mapping.

### What happened to MATIC?

Polygon replaced it with POL as the native gas and staking token of Polygon PoS on 4 September 2024, automatically on that chain and through a one-to-one migration contract for MATIC held on Ethereum. The data vendors did not rename the old record. They gave POL a new ID and left MATIC behind, so a price series keyed to the old ID stops rather than continuing under the new ticker.

### Is an Ethereum contract address case-sensitive?

No, but it is printed as if it were. ERC-55 encodes a checksum in the capitalisation of the hex letters, so the same address arrives in lower case from one vendor and mixed case from another. Lower-case hex addresses before comparing them, and only hex ones — a Solana address is base58, where the case is part of the value.

## Sources

1. [Coins List (ID Map)](https://docs.coingecko.com/reference/coins-list) — CoinGecko, read 2026-09-26
2. [Coin Data by Token Address](https://docs.coingecko.com/reference/coins-contract-address) — CoinGecko, read 2026-09-26
3. [Coin Price by IDs, names or symbols](https://docs.coingecko.com/reference/simple-price) — CoinGecko, read 2026-09-26
4. [Cryptocurrency endpoints — ID Map and Metadata](https://coinmarketcap.com/api/documentation/pro-api-reference/cryptocurrency) — CoinMarketCap, read 2026-09-26
5. [Manual — Symbols And Market Ids, Naming Consistency, Unified Networks](https://github.com/ccxt/ccxt/wiki/Manual) — CCXT, read 2026-09-26
6. [DefiLlama Free API — Coins and Prices](https://api-docs.defillama.com/) — DefiLlama, read 2026-09-26
7. [List active symbol mapping for the exchange](https://www.coinapi.io/products/market-data-api/docs/rest-api/metadata/symbols/map/exchange_id/get) — CoinAPI, read 2026-09-26
8. [Save the Date: MATIC to POL Migration Coming September 4th](https://polygon.technology/blog/save-the-date-matic-pol-migration-coming-september-4th-everything-you-need-to-know) — Polygon Labs, 2024-07-18
9. [ERC-55: Mixed-case checksum address encoding](https://eips.ethereum.org/EIPS/eip-55) — Ethereum Improvement Proposals, 2016-01-14. Final status, and it is still the form in which wallets and block explorers print Ethereum addresses.

*Last updated 2026-09-26. Corrected in place — an endpoint that moves is a bug here, not a new post.*
