Skip to content

Curating token lists

"USDT is missing on chain X" is almost never a missing record. It is a ranking problem, and there is a script for it.

Why tokens disappear

GET /common/token/:chainId
  → returns tokens WITH `order` first (ascending), then the rest in natural order

Frontend token modal
  → shows only the FIRST 200
  → then re-sorts by the user's holdings

Most chains never had order set on their stablecoins

So USDT and USDC sit deep in a multi-thousand-token list and never appear by default — only via search. The record exists; the user cannot see it.

The fix

bz-backend/scripts/curate-token-order.js

bash
# preview (reads the DB and LiFi, writes NOTHING)
node --env-file=.env scripts/curate-token-order.js

# apply, all chains
node --env-file=.env scripts/curate-token-order.js --apply

# one chain only
node --env-file=.env scripts/curate-token-order.js 56 --apply

Safe by default

Dry-run unless you pass --apply. Always run the preview first and read the diff.

How it decides what is a major

Two signals, combined — and the combination is the point.

1. LiFi's per-chain curated list

GET https://li.quest/v1/tokens?chains={chainId}

LiFi's list is curated majors-first, so it is a reasonable source of truth for "which tokens matter on this chain".

2. A hardcoded priority ladder

js
const PRIORITY = [
  // Stablecoins — what users bridge and swap most
  'USDC','USDT','USDCE','USDBC','USDT0','USDE','USDS','DAI',
  'FRAX','TUSD','PYUSD','USDD','GUSD','USDG','CUSD','XDAI',
  // BTC
  'WBTC','CBBTC','LBTC','TBTC','BTCB',
  // ETH and LSTs
  'WETH','WSTETH','WEETH','STETH','RETH','CBETH','EZETH','RSETH',
  // Wrapped natives
  'WBNB','WMATIC','WPOL','WAVAX','WS','WSEI','WCELO','WRON','WAPE',
  'WGLMR','WFTM','WMNT','WXTZ','WBERA','WHYPE',
  // Bluechips
  'LINK','UNI','AAVE','ARB','OP','CRV','LDO','MKR','PEPE','SHIB',
]

Native always ranks 0; these follow at index + 1.

Symbol normalisation

js
const norm = (s) => (s || '').toUpperCase().replace(/[^A-Z0-9]/g, '')

So USDC.e, USD₮0 and USDCe all collapse onto one priority entry. Without this, bridged variants would each need their own list entry.

Scam-clone protection

Both signals must agree

A token is promoted only if it appears in LiFi's curated list for that chain and matches the priority ladder. Matching is by canonical address, case-insensitively.

That is what stops a scam token reusing the symbol USDC from being promoted to the top of the picker — which would be considerably worse than USDC being hard to find.

Adding a token to the priority ladder

Edit PRIORITY in the script. Position matters — it becomes the rank.

js
const PRIORITY = [
  'USDC', 'USDT', 'MYSTABLE',   // ← inserted at rank 3

]

Then dry-run, read the diff, and apply.

A token still needs to be in LiFi's list

Adding a symbol to PRIORITY promotes nothing if LiFi does not list it for that chain. For a token LiFi does not know about, set order by hand on the Token document.

Setting order manually

For a one-off:

js
await Token.updateOne(
  { chainId: 1234, address: '0x…'.toLowerCase() },
  { $set: { order: 5 } }
)

Low numbers surface first. Native is 0; the curated majors occupy roughly 1–60. Anything above 200 is effectively invisible by default.

Seeding a new token

js
{
  address: '0x…',              // lowercase
  chainId: 1234,
  symbol: 'MYC',
  decimals: 18,
  name: 'MyCoin',
  logoURI: '…',
  coingeckoId: 'my-coin',      // needed for the fee summary
  cmcId: 12345,                // needed for Autopilot prices
  order: 20,                   // ← do not omit
}

Two price-source ids, and neither is a superset

coingeckoId feeds GET /defi/fees/summary; cmcId feeds the Autopilot price series. A token with neither works for swapping but will not appear in the fee summary and cannot be used in an Autopilot policy that needs a price.

Cache busting

Token lists are cached. After changing order or seeding records:

bash
node scripts/clearTokenCache.js

Otherwise the change takes effect only after the cache expires.

Other token scripts

bz-backend/scripts/:

ScriptPurpose
curate-token-order.jsThe main tool. Dry-run by default
clearTokenCache.jsBust the cache
check-token-location.jsWhere a token record lives
checkDatabase.js, checkDb.jsGeneral inspection
alchemyMongo.jsAlchemyPay catalogue sync
fetchAllChainBalances.jsBalance inspection

Provider catalogues are separate

Do not confuse token visibility with fiat provider catalogues, which are cached in their own collections and refreshed by administrative endpoints:

CollectionRefreshed by
alchemyCrypto, alchemyFiatGET /defi/alchemy/save-crypto, /save-fiat
changellyCrypto, changellyFiatGET /defi/changelly/save-crypto, /save-fiat
onRampCryptoGET /defi/onramp/save-crypto
transacCrypto, transacFiatGET /defi/transak

There is no refresh cron for these

"Provider X supports token Y but Blazpay does not offer it" in the fiat flow is a stale catalogue, and someone has to call the endpoint. See fiat API.

Diagnosing "my token is missing"

□ Does a Token record exist for that (address, chainId)?
    → No:  seed it
    → Yes: continue
□ Is `order` set, and below ~200?
    → No:  run curate-token-order.js, or set it manually
□ Was the cache busted after the change?
    → No:  clearTokenCache.js
□ Is the chain's Network record `show: true`?
□ Is the user searching, or browsing? Search bypasses the 200 slice
□ Is it a fiat flow? Then it is a provider catalogue, not Token.order

Why the slice exists at all

200 tokens is a payload-size and render-performance limit on a multi-thousand-token chain. The correct long-term fix is server-side search plus pagination rather than a curated head — but the curation script is the cheap fix, and it addresses the actual complaint (majors missing) rather than the general one (long-tail hard to find, which search already solves).

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.