CLI tool
MetaMask/contract-metadata avatar
MetaMask/contract-metadata

contract-metadata maps CAIP-19 asset identifiers to token names and logos

A mapping of ethereum contract addresses to broadly accepted icons for those addresses.

586 stars1,437 forksJavaScriptISC

At a glance

What is it?
The MetaMask contract-metadata package is a lookup table from checksummed contract addresses to token names, symbols and icon files, and the project now states that it is effectively frozen in favour of EIP-747 token registration. It also ships a CLI for adding and verifying assets against the CAIP-19 format.
Who is it for?
Adopt contract-metadata if you are building a wallet interface and want a bundled set of token names and icons without maintaining your own list, because the package is published to npm and covers a broad set of already listed assets. Do not adopt it as the mechanism for listing your own new token, since the README states the repository is effectively frozen and directs developers of new tokens to EIP-747, which asks the user for permission at the wallet level.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the package actually contains, and the two identifier formats it depends on

The package is a lookup table and nothing more elaborate. The README describes it as a mapping of checksummed Ethereum contract addresses to metadata, like names, and images of their logos, and the package.json description says the same thing in slightly different words.

Two format decisions do all the work. Address keys follow the EIP 55 address checksum format, and file keys follow the CAIP 19 asset id format. EIP 55 is what makes a key unambiguous: an Ethereum address is a hex string that is valid in mixed case or lowercase, so without a checksum there would be two possible keys for one contract. The CAIP-19 identifier is the chain-aware form, and it looks like this:

The key is built as `eip155:${chainId}/erc20:${toChecksumAddress(address)}`

The namespace, then a slash, then the asset namespace and the checksummed reference. The chain id is part of the key, so the same contract address on Ethereum mainnet and on a test network is two separate entries rather than a collision.

The repository layout shows the split between data and tooling. There is an icons folder, a metadata folder holding one JSON file per asset, a labels folder, a contract-map.json that is the built index, index.js as the package main entry, buildindex.js to rebuild the map, and cli-update-asset.js for the command line tool. There are also generated files with more specific purposes, including a metamask-uniswap-tokenlist.json and a generate-multichain-uniswap-standard-list.js script, which is the Uniswap token list format layered on top of the same data.

The published package only ships a subset of that. The files array in package.json lists index.js, contract-map.json and images, so a consumer gets the built map and the images, not the metadata source files or the build scripts.

Reading the map from code, and why the checksum step is not optional

The usage example in the README is short, and every part of it earns its place. You import the map and a checksum helper, build a CAIP-19 key, look it up, and render an image element if a logo exists.

javascript
import contractMap from '@metamask/contract-metadata'
import ethJSUtil from 'ethereumjs-util'
const { toChecksumAddress } = ethJSUtil

function imageElForEVMToken (chainId, address) {
  const caip19Address = `eip155:${chainId}/erc20:${toChecksumAddress(address)}`
  const metadata = contractMap[caip19Address]
  if (metadata?.logo) {
    const fileName = metadata.logo
    const path = `${__dirname}/${metadata.logo}`
    const img = document.createElement('img')
    img.src = path
    img.style.width = '100%'
    return img
  }
}
// to get ethereum erc20 token img el
const ethereumNetworkChainId = 1
imageElForEVMToken (ethereumNetworkChainId, "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d")

The toChecksumAddress call is the load-bearing line. Because the map's keys are checksummed, an address passed in lowercase will not match a key and the lookup will return undefined. The function returns undefined for a missing logo rather than throwing, so a miss in the map degrades to a token with no icon rather than to a broken render.

The path construction is worth noting because it affects your build. The logo is resolved against __dirname with the metadata path appended, so the icons are read from disk relative to the installed package rather than fetched from a CDN. For a web application that means the bundler has to be able to resolve and copy those files, and for a server-side renderer it means the path points into node_modules.

The ethereumjs-util dependency is a devDependency in the package.json, so the example is illustrative rather than something you inherit when you install the package. You supply your own checksum function.

The CLI for adding assets, and what it does on your behalf

The repository includes a CLI tool, cli-update-asset.js, to help manage contract metadata and icons following the CAIP-19 standard, and it is exposed through npm scripts in the package.json.

json
    "asset": "node cli-update-asset.js",
    "asset:set": "node cli-update-asset.js set",
    "asset:verify": "node cli-update-asset.js verify",
    "asset:list": "node cli-update-asset.js list"

The set command takes a required --caip identifier, and for a new asset a --name, --symbol, --decimals and --image, plus optional --labels. This adds a new asset:

bash
npm run asset:set -- \
  --caip "eip155:1/erc20:0xTOKEN_ADDRESS" \
  --name "Token Name" \
  --symbol "SYMBOL" \
  --decimals 18 \
  --image ./path/to/logo.svg

The same command updates an existing asset, and passing only --name changes the name without touching the rest, and passing --labels sets labels. The --image parameter accepts a local path or an HTTP/HTTPS URL, and the supported extensions are .svg, .png, .jpg and .jpeg.

What the tool does automatically is the part that matters for trusting it. It validates all inputs using Zod schemas, so a malformed identifier or a missing required field fails before anything is written. It renames icon files to match the asset ID, and removes old icons when updating with a new image, which stops a directory accumulating orphaned files. It auto-detects the erc20 and spl fields based on the asset namespace, so you do not have to pass --erc20 for an erc20: key. And it downloads and saves images from URLs with the correct extension.

Two commands support the workflow around an edit. Verification checks an asset by its CAIP identifier:

bash
npm run asset:verify -- \
  --caip "eip155:1/erc20:0xTOKEN_ADDRESS"

And listing shows everything in a namespace:

bash
# List all assets in a namespace
npm run asset:list -- --namespace "eip155:1"

The documented workflow is: add or update with asset:set, verify with asset:verify, rebuild the contract-map.json with node buildindex.js, review the changes in git, then commit and push.

The metadata file format, and a typo you will meet in the sample

Each asset has its own JSON file in the metadata folder, named after the CAIP-19 asset ID, and the submission criteria describe adding the logo image to the icons folder in .svg or .png format alongside a metadata file.

The sample submission shows the shape of a metadata entry:

json
{
  "eip:155/erc20:0x6090A6e47849629b7245Dfa1Ca21D94cd15878Ef": {
    "name": "ENS Registrar",
    "decimals": 18,
    "symbol": "ENS",
    "erc20": true,
    "logo": "./icons/eip:155/erc20:0x6090A6e47849629b7245Dfa1Ca21D94cd15878Ef.svg"
  }
}

Tokens should include a field "erc20": true, and may include symbol, described as a five-character or less ticker, and decimals, the precision of the tokens stored. The full list of permitted fields lives in permitted-fields.json in the repository root, which is the authoritative answer on what an entry may contain.

Notice the key in that sample: eip:155/erc20:..., with a colon after eip. The format the rest of the document describes and the code builds is eip155:1/erc20:0x..., a namespace with no colon and a chain id after it. The sample in the README does not match the format it is illustrating, and the logo path in the same sample repeats that shape. That is a documentation inconsistency in the repository rather than something to copy, and it is worth knowing before you go looking for a bug in your own key construction.

The icon requirements are specific and reasonable: the icon should be small, square, but high resolution, and a vector/svg or a png, with a style guide referenced for best practices.

Why the project calls itself frozen, and what EIP-747 replaces it with

The most important line in the README is the one that says this repository is effectively frozen. The project recommends that developers of new tokens use EIP-747 to ask the user's permission to display your tokens in their wallet, and gives two reasons: it reduces the dangers of airdrop-based phishing, and it reduces administrative overhead from managing this list.

Both reasons are about trust, and they point in the same direction. A static list of tokens is a list someone else decides what is in, and an airdropped impostor token gets a legitimate-looking icon if the list says so. Moving the decision to the wallet means the user is asked, per token, in a flow the wallet already owns.

The submission process section is candid about the maintenance burden behind that reasoning. Maintaining this list is a considerable chore, it is not the highest priority, and inclusion is not guaranteed on any urgent timeline. The project says it is actively looking for fair and safe ways to maintain a list like this in a decentralized way, because maintaining it is a large and security-delicate task.

So the list still works and is still published, it is just no longer where new listings come from. The practical consequence is that a token you launched last week will not be in this map, and no pull request will get it in on a schedule you can plan around.

There is a separate concern for assets with unusual addressing. The criteria state that the address should be in checksum format or it will not be accepted, and that this is true of non-evm assets as well, since some network's addressing formats are case-sensitive. A list keyed on address therefore has to get case handling right for every chain it covers, which is one reason the key format is CAIP-19 rather than a bare address.

What the acceptance criteria exclude, and where this project is the wrong answer

Three limits, and the first is a hard exclusion.

The first is scope. Assets must not be ERC-721s or ERC-1155s, so this list covers fungible tokens only and is not a source for NFT artwork or metadata. Alongside that, the criteria impose a gate that is specific to Etherscan: an asset must have a NEUTRAL or OK reputation there if it is an Ethereum-network asset, with other EVM networks verified as possible. A pull request also has to link to the official project website, that website has to include an explanation of the project, the project needs clear signs of activity, either traffic on the network, activity on GitHub, or community buzz, and a verified source code on a block explorer is a nice-to-have. Those criteria exist because the list is a trust signal, and they are the reason inclusion takes time.

The second is that the data is a snapshot of one organisation's judgement. A list curated by a single wallet vendor will reflect that vendor's user base, and a token popular in a community the list maintainers do not follow will be missing. The same applies to the mirror image problem: a list that is too permissive is an attack surface for phishing, and the Etherscan reputation requirement is a partial answer to that rather than a complete one.

The third is the operational cost of relying on it. The icons are read from the installed package, so your build has to copy them, and if you want icons for tokens not in the map you need a second source anyway. A wallet product that wants live, permissioned token metadata is not going to get it from this package.

The alternative approach is EIP-747, which the project itself points to for new tokens. The difference in approach is where the authority sits. This list is centrally curated, published as a static JSON file, and consumed by looking up a key you already know. EIP-747 inverts that: the token's issuer declares the display metadata, and the wallet asks the user to approve it, so the data travels with the token rather than in a list someone maintains. If your requirement is showing a token you issued, or a token your users received, that is the mechanism to use, and this package is at best a fallback for the tokens that were already listed before the freeze.

Editorial conclusion

Adopt contract-metadata if you are building a wallet interface and want a bundled set of token names and icons without maintaining your own list, because the package is published to npm and covers a broad set of already listed assets. Do not adopt it as the mechanism for listing your own new token, since the README states the repository is effectively frozen and directs developers of new tokens to EIP-747, which asks the user for permission at the wallet level. Verify first that the asset you need is present under its CAIP-19 identifier with the chain id you use, that the published version 2.5.0 matches the node version you run, and that the logo path resolves from your own bundle, since the README's own example reads the file relative to __dirname rather than serving it over the network.

Frequently asked questions

How do I install and use @metamask/contract-metadata?

Install it from npm with npm install @metamask/contract-metadata, then import the default export as contractMap. Build a CAIP-19 key of the form eip155:${chainId}/erc20:${checksummedAddress} and look it up to get the token metadata and logo path.

Why does my token lookup return nothing in contract-metadata?

The map's keys are checksummed, so an address in lowercase will not match a key. The key must also include the chain id, since the same address on different chains is a different entry, and the asset may simply not be listed because the repository is effectively frozen.

How do I add a new asset with the contract-metadata CLI?

Run npm run asset:set with --caip, and for a new asset --name, --symbol, --decimals and --image. Then run npm run asset:verify with the same --caip value, rebuild the index with node buildindex.js, and review the changes in git before committing.

Is the MetaMask contract-metadata list still being updated?

The README states the repository is effectively frozen and recommends that developers of new tokens use EIP-747 to ask the user's permission to display tokens in their wallet, which reduces airdrop-based phishing risk and the overhead of maintaining the list. The last tagged release is v2.5.0 from 2024-04-04.

What fields can a contract-metadata entry contain?

Tokens should include an "erc20": true field and can include symbol, described as a five-character or less ticker, and decimals, the precision of the tokens stored. The complete list of permitted fields is in the permitted-fields.json file at the repository root.

Can I submit an ERC-721 or NFT to the contract-metadata list?

No. The submission criteria state that assets must not be ERC-721s or ERC-1155s, and an Ethereum-network asset must have a NEUTRAL or OK reputation on Etherscan.

Official sources

  1. Issues
  2. License: ISC
  3. MetaMask/contract-metadata on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/metamask-contract-metadata.svg)](https://hysenlabs.com/projects/metamask-contract-metadata)