Skip to content

About

The tokens we add manually to the interface

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

218 Commits

Folders and files

Repository files navigation

Ekubo token list

This repository is the authoritative, auditable source for the token metadata written to Ekubo's database. The database synchronization command reads only committed files from this repository; it never downloads third-party token lists.

Files

  • curated-tokens.json is the human-maintained, highest-precedence input. Its document can contain both curated token rows and curated bridge mappings.
  • tokens.json is the complete generated document consumed by the database sync. Its top-level tokens and bridge_relationships arrays keep both kinds of authoritative metadata in one reviewable file.
  • token-list.schema.json is the JSON Schema for both curated and generated token-list documents, including nullable raw total_supply and circulating_supply values.
  • token-sources.json records the winning source for every generated token.
  • image-sources.json records upstream-to-Cloudflare logo migrations so repeated runs do not upload the same image again.
  • coingecko-markets.json caches the circulating and total supply CoinGecko reports for every referenced coin, alongside the rotation cursor described below.
  • src/sources.ts is the reviewable catalog of every remote token source.

Cross-chain links

bridge_relationships links a token to the same asset on another chain. Each link is directed and keyed by its source token and destination chain, so a token has at most one counterpart per chain. The database sync writes all of these fields; the five original ones are what the API's bridgeInfos was built on, and the rest are additive metadata:

Field Meaning
source_chain_id, source_token_address The token the link starts from.
dest_chain_id, dest_token_address Its counterpart. Both tokens are always rows in the generated list.
source_bridge_address The bridge or escrow on the source chain when the source names one, else null. It is informational; relationship says what the link is.
relationship canonical_bridge: minted by the destination chain's official bridge (OP Stack standard bridge, Arbitrum gateway, StarkGate). native_issuer: the issuer's own deployment on both chains (Circle's USDC). interoperability: a version linked by an interoperability protocol such as LayerZero OFT, Wormhole NTT, or Chainlink CCIP, which is not a canonical bridge copy. unknown: the source links the two but does not say how.
bridge_name The bridge, issuer, or protocol, when known.
source_name, source_url The source that claimed the link and where it was read.
inferred_reverse true when the source stated only the opposite direction and the link mirrors it.

Links come from these sources. The first source to claim a key wins, and a later source that names a different counterpart is logged as a conflict instead of overwriting it:

  1. Curated links in curated-tokens.json.
  2. Circle's published USDC and EURC contract tables and Tether's supported protocols page (native_issuer).
  3. Native ether on rollups that bridge it from Ethereum, and the OP Stack WETH predeploy that wraps it (canonical_bridge), from CANONICAL_ETH_ROLLUPS in src/sources.ts.
  4. StarkGate's bridged-token list, the Superchain token list, Offchain Labs' Arbitrum gateway lists, and Ava Labs' Avalanche Bridge resources (canonical_bridge).
  5. Mappings read from the bridge contracts into bridge-reads.json (canonical_bridge): the Arbitrum One and Robinhood Chain gateways (the token's l1Address(), confirmed by the Ethereum gateway router), the Polygon PoS RootChainManager, and the OP Stack standard bridge (the token's remoteToken() with the bridge predeploy as its bridge()). An Ethereum token claimed by two listed tokens on one chain is left out as ambiguous. The daily build reads this snapshot without an RPC endpoint; bun run read-bridges refreshes it.
  6. The USDT0 Network deployments API for USDT0 and XAUT0, LayerZero OFTs backed by USDT and XAUT locked on Ethereum (interoperability).
  7. The bridgeInfo extension of the Uniswap default list (unknown, because Uniswap links the asset people mean on each chain whether it is an issuer deployment, a bridge copy, a custodial peg, or an OFT).
  8. The Monad list's crossChainAddresses, labelled by the protocol it names (interoperability, else unknown).

A shared symbol, name, or CoinGecko ID never creates a link. A link is kept only when both tokens are in the list, never links a chain to itself, and is mirrored when the reverse key is free. A source that cannot be read keeps its previous links. Each run logs what it dropped, the conflicts, the links still without a reciprocal, and any chain where two tokens claim the same canonical or issuer relationship to one counterpart (a canonical copy beside an issuer deployment, such as USDC.e beside USDC, is expected and not reported).

bun run update-bridges regenerates only the links in tokens.json from these sources, without database, CoinGecko, or Cloudflare credentials.

tokens.json is the only source of links in the database. The sync upserts every link and then deletes the links the API serves (those whose source token has a token row) whose source token and destination chain are no longer in tokens.json, printing each deleted row so it can be restored. Rows whose source token has no token row are never served and are left alone. If that would delete more than 20 percent of the served links, or the list has no links at all, it deletes nothing, still applies the upserts, and fails so the workflow reports it.

Token address zero is a chain's native currency, the asset its gas is paid in. normalizeTokenAddress folds 0x0, 0xEeee…eEEeE, and 0x455448 into that one address, so a chain has exactly one native row. NATIVE_CURRENCIES in src/sources.ts names the asset for every chain a list is fetched for, curated-tokens.json carries the row itself rather than inheriting it from a third-party list, and validation fails if a curated row is missing or if any source names a different asset at address zero — ETH on Berachain, say. Tempo is deliberately absent: it has no native currency, and gas there is paid in stablecoins.

This list is mainnets only. Ekubo removed testnet support across the interface, the indexer and the quoter service, so a testnet row here would describe a chain nothing in the stack can serve. NATIVE_CURRENCIES therefore carries no testnet, and validateNativeCurrencies makes that binding in both directions for curated-tokens.json: a curated row at address zero on a chain the list does not name fails, and so does a named chain with no curated row.

Generation uses first-source-wins precedence:

  1. Ekubo's curated tokens.
  2. Tokens registered through Ekubo's onchain token registry.
  3. The standard token lists in their declared order in src/sources.ts.
  4. CoinGecko Pro token lists for explicitly mapped production chains.
  5. AVNU's Starknet lists.

Because the first source wins, a wrong field in an upstream list is corrected with a curated row rather than by reordering sources. Upstream lists do sometimes attach another project's logo to a token. The Uniswap default list, for example, gives Velodrome Finance's VELO on Optimism (0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db) the CoinGecko image of the unrelated Velo token on BNB Smart Chain, although CoinGecko itself maps that address to Velodrome. The curated row points at Velodrome's own logo, committed under assets/, and a test pins the two tokens to different logos. A comparison of every generated logo with CoinGecko's image for the same chain and address found three more such tokens from the 1inch list (OneRing on Fantom, and BNBx and Binance Beacon ETH on BNB Smart Chain); they are curated with CoinGecko's image.

The explicit CoinGecko Pro mappings cover every chain the Ekubo wallet MCP server configures on first run, because that wallet vendors tokens.json as its compiled-in default token list and a missing chain leaves it showing bare addresses on an approval screen. WALLET_DEFAULT_NETWORK_CHAIN_IDS in src/sources.ts pins that set, and a test fails if one of those chains loses its source.

The updater starts from the curated inputs each time. Removed remote tokens do not linger merely because they appeared in an older generated file. Git commits show every metadata, provenance, bridge, and hosted-logo change.

After token discovery, the updater combines CoinGecko IDs declared by an input source with exact chain-and-contract mappings from /coins/list?include_platform=true, then reads total_supply and circulating_supply from batched /coins/markets requests. The reviewed platform mappings in src/sources.ts are checked against CoinGecko's /asset_platforms response before enrichment. Ambiguous contract mappings are skipped. Existing curated or onchain-registered total supply wins; CoinGecko fills missing totals and circulating supply. CoinGecko reports supply in whole-token units, so the updater converts it to an integer in the token's indivisible units before writing JSON.

Reading every referenced coin's supply on every run was this repository's largest source of CoinGecko requests, so supplies are cached in coingecko-markets.json instead. A run refreshes the coins that are new to the cache plus one rotation slice of the rest, which covers every coin within rotation_slots runs — a week at the daily schedule — for a small fixed cost per run. The cursor lives in the artifact so the schedule survives across runs. A coin CoinGecko returns nothing for is cached as empty rather than dropped, so it is not re-requested until its slot comes round again, and coins no longer referenced by any token are pruned. Deleting the file is safe: the next run rebuilds it in one full pass.

Supply is best-effort market metadata, not an onchain accounting invariant. CoinGecko may report a global economic supply for an asset deployed on multiple chains, and circulating supply can change between updates. A missing, invalid, ambiguous, or unavailable value remains null. Consumers compute FDV and market cap as usd_price * raw_supply / 10 ^ token_decimals and should preserve the distinction between unknown and zero.

Token jurisdiction policy

jurisdiction-policy/ekubo-token-jurisdictions-v2.json is the token jurisdiction policy decided on EKU-853 (CLO, 2026-10-06). The country lists are a minimum product-policy floor, not a legal determination. The MCP server (src/jurisdiction-policy.json) and the interface (src/util/common/jurisdictionPolicy.json) vendor it byte-for-byte, and all three repositories pin its SHA-256 in a test.

On Robinhood Chain (4663) every address falls into one of three groups:

  • rhj_stock_token: Robinhood Assets (Jersey) Stock Tokens, restricted for buying and selling alike in AE, BY, CA, CH, CU, GB, IR, KP, MM, RU, SD, SG, SS, SY, UA, US and VE, and when the country is unknown.
  • non_class: ETH, WETH, USDG and STONX, verified not to be Stock Tokens. Adding an address here needs on-chain verification and CTO sign-off.
  • anything else is unknown, and consumers hold it in every country.

Every entry carries provenance (source, ref, observed_at). Regenerate with bun run jurisdiction-policy --observed-at <ISO-8601> --issuer <rhj-assets.json> --curated curated-tokens.json --curated-commit <sha>. Class membership only grows and keeps each entry's first observation; removing an address needs a CLO reclassification, not a list edit. bun run validate fails when curated-tokens.json has a chain-4663 row in neither group, and the daily Jurisdiction policy registry check workflow fails when the issuer registry (https://api.robinhood.com/rhj/assets) lists an address the policy does not classify.

Automation

Update authoritative token list runs daily, on a push to main that changes an input, and through workflow_dispatch. It fetches all declared sources, reads onchain registrations through a read-only database connection, hosts every available external logo in Cloudflare Images, validates the result, and commits changed generated files to main. JSON Schema validation runs before a generated document can be committed.

Sync authoritative token list to database is a separate workflow. It runs after a human push that changes the list, after a successful update workflow, every 30 minutes as a reconciliation guard, and through workflow_dispatch. The sync validates and then upserts both collections from tokens.json in one transaction, in which it also removes links and token rows no longer in the list.

Token rows are pruned only where the token picker shows them by default (visibility priority 0 or higher) and only on chains the list covers. Hidden rows (priority below 0) predate the reviewed list and still name tokens in pool statistics, so they are left alone, as are rows on chains absent from the list. Each deleted row is printed in full so it can be restored, and its price history is deleted with it because that history references the row; the price worker rebuilds prices if the token is listed again. If a sync would delete more than 20 percent of the visible rows in scope, or more than 20 percent and more than 100 rows on any one chain, it deletes no token rows, still applies the upserts, and fails so the workflow reports it. The workflow_run trigger is intentional: pushes made with GitHub's automation token do not trigger a second ordinary push workflow.

Configure these repository secrets:

  • PG_CONNECTION_STRING: the single Postgres connection used to read latest_token_registrations_view while generating the list and to write the committed list during database synchronization.

  • CLOUDFLARE_ACCOUNT_ID: Cloudflare account containing Ekubo Images.

  • CLOUDFLARE_API_TOKEN: token with Cloudflare Images Write permission.

  • COINGECKO_API_KEY: CoinGecko Pro API key. The updater sends it only in the x-cg-pro-api-key request header. It is used for token lists, exact contract mapping, and batched market-supply enrichment.

    A token-list source that cannot be downloaded after every retry does not fail the run. The updater skips the source — a dead standard list, a missing AVNU or bridge page, or the CoinGecko Pro token lists — and folds the previous tokens.json back in, so the outage degrades the list to the last known good rows instead of deleting the tokens the missing source contributed. A skipped run logs Skipping <source> …; that line means the published list is stale for that source. A CoinGecko 429 that survives every retry is additionally reported as CoinGecko is rate limited (429): SKIPPING …, which means the published list is stale, so check the key's plan and quota when it appears. The CoinGecko supply refresh still skips only on 429; every other supply failure still fails the run.

Configure these repository variables:

  • CLOUDFLARE_IMAGES_DELIVERY_HASH: the account delivery hash used in https://imagedelivery.net/<hash>/....
  • CLOUDFLARE_IMAGES_VARIANT: the public token-logo variant; defaults to logo.
  • CLOUDFLARE_IMAGES_REQUEST_INTERVAL_MS: minimum delay between Cloudflare Images API requests when the batch API is unavailable; defaults to 300 milliseconds to stay below the global API limit.

Cloudflare uploads use a deterministic custom ID derived from the upstream URL. That makes retries idempotent and lets tokens on multiple chains share a hosted image. The updater checks that deterministic delivery URL before uploading, uses Cloudflare's batch upload API when available, paces its fallback API requests, and honors rate-limit retry headers. If Cloudflare cannot fetch an upstream URL, the runner downloads the image and uploads its bytes. IPFS image sources are resolved through an HTTP gateway. Every generated URL is normalized to the configured variant. A failed refresh retains the token's previously hosted image; otherwise an unreachable logo is recorded as null without failing the entire token-list update.

CoinGecko enrichment first checks /asset_platforms against the explicit platform-to-chain mappings in src/sources.ts, then downloads each mapped chain's standard token list from /token_lists/{asset_platform_id}/all.json. The updater rejects a source if CoinGecko changes the mapping or returns a token for another chain. Supply enrichment uses one complete /coins/list request and bounded batches of up to 200 coin IDs per /coins/markets request; it never makes one request per token. Requests are sequential and use the shared retry and rate-limit handling.

Alchemy's ERC-20 Token API is intentionally not used for this enrichment. Its token metadata response provides name, symbol, decimals, and logo, while its other fungible-token endpoints are wallet balance and allowance queries. It does not currently provide ERC-20 circulating supply, total supply, or holder count. NFT owner endpoints are not applicable to fungible tokens.

The indexer migration that adds erc20_tokens.circulating_supply must be applied before this repository's database sync is deployed. The sync writes both supply columns and deliberately fails instead of silently dropping the new field when the target schema is stale.

Likewise, indexer migration 00136_erc20_bridge_link_provenance, which adds the link metadata columns to erc20_tokens_bridge_relationships, must be applied before the sync that writes them is deployed.

Local development

Install and validate:

bun install
bun run format:check
bun run lint
bun run typecheck
bun test
bun run validate

Regenerate the list by exporting the workflow environment variables and running:

bun run update-tokens

For source-processing development without a token-registry database, pass --without-registrations directly:

bun scripts/update-tokens.ts --without-registrations

Synchronize the committed list by setting PG_CONNECTION_STRING and running bun run sync-database.

About

The tokens we add manually to the interface

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages