Appearance
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 holdingsMost 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 --applySafe 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.jsOtherwise the change takes effect only after the cache expires.
Other token scripts
bz-backend/scripts/:
| Script | Purpose |
|---|---|
curate-token-order.js | The main tool. Dry-run by default |
clearTokenCache.js | Bust the cache |
check-token-location.js | Where a token record lives |
checkDatabase.js, checkDb.js | General inspection |
alchemyMongo.js | AlchemyPay catalogue sync |
fetchAllChainBalances.js | Balance 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:
| Collection | Refreshed by |
|---|---|
alchemyCrypto, alchemyFiat | GET /defi/alchemy/save-crypto, /save-fiat |
changellyCrypto, changellyFiat | GET /defi/changelly/save-crypto, /save-fiat |
onRampCrypto | GET /defi/onramp/save-crypto |
transacCrypto, transacFiat | GET /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.orderWhy 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).