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.