Shared Lists
Shared lists eliminate duplication of common value sets across schemas. Instead of every Etherscan schema maintaining its own chain list, they reference a single evmChains shared list that is injected at load-time.
Purpose
Section titled “Purpose”Many schemas across different providers need the same sets of values — EVM chain identifiers, fiat currency codes, token standards. Without shared lists, each schema duplicates these values inline, leading to:
- Inconsistency — one schema uses
'eth', another uses'ETH', a third uses'ethereum' - Maintenance burden — adding a new chain means updating dozens of schemas
- No single source of truth — no way to verify which values are canonical
flowchart LR A[Shared List: evmChains] --> B[etherscan/contracts.mjs] A --> C[moralis/tokens.mjs] A --> D[defillama/protocols.mjs] B --> E["filter: etherscanAlias exists"] C --> F["filter: moralisChainSlug exists"] D --> G["filter: defillamaSlug exists"]A single shared list feeds multiple schemas. Each schema applies its own filter to extract only the entries relevant to that provider.
List Definition Format
Section titled “List Definition Format”A shared list is a .mjs file that exports a list object with two top-level keys: meta and entries.
export const list = { meta: { name: 'evmChains', version: '1.0.0', description: 'Unified EVM chain registry with provider-specific aliases', fields: [ { key: 'alias', type: 'string', description: 'Canonical chain alias' }, { key: 'chainId', type: 'number', description: 'EVM chain ID' }, { key: 'name', type: 'string', description: 'Human-readable chain name' }, { key: 'etherscanAlias', type: 'string', optional: true, description: 'Etherscan API chain parameter' }, { key: 'moralisChainSlug', type: 'string', optional: true, description: 'Moralis chain slug' }, { key: 'defillamaSlug', type: 'string', optional: true, description: 'DeFi Llama chain identifier' }, { key: 'coingeckoPlatformId', type: 'string', optional: true, description: 'CoinGecko asset platform ID' } ], dependsOn: [] }, entries: [ { alias: 'ETHEREUM_MAINNET', chainId: 1, name: 'Ethereum Mainnet', etherscanAlias: 'ETH', moralisChainSlug: 'eth', defillamaSlug: 'Ethereum', coingeckoPlatformId: 'ethereum' }, { alias: 'POLYGON_MAINNET', chainId: 137, name: 'Polygon Mainnet', etherscanAlias: 'POLYGON', moralisChainSlug: 'polygon', defillamaSlug: 'Polygon', coingeckoPlatformId: 'polygon-pos' } // ... more entries ]}Meta Block
Section titled “Meta Block”The meta block describes the list identity and structure.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique list identifier (camelCase) |
version | string | Yes | Semver version |
description | string | Yes | What this list contains |
fields | array | Yes | Field definitions for entries |
dependsOn | array | No | Dependencies on other lists |
Naming Convention
Section titled “Naming Convention”List names use camelCase and must be globally unique. The name should describe the collection, not a single entry:
evmChains(notevmChain)fiatCurrencies(notfiatCurrency)isoCountryCodes(notcountryCode)
Versioning
Section titled “Versioning”Lists follow strict semver:
| Bump | When |
|---|---|
Patch (1.0.1) | Correcting a typo, fixing a wrong value |
Minor (1.1.0) | Adding new entries, adding new optional fields |
Major (2.0.0) | Removing entries, removing fields, renaming fields, changing types |
Schemas pin to a specific version. A major version bump requires all referencing schemas to update.
Field Definitions
Section titled “Field Definitions”Each entry in meta.fields describes one field that entries can or must contain.
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Field name |
type | string | Yes | string, number, or boolean |
description | string | Yes | What this field represents |
optional | boolean | No | If true, entries may omit this field |
Only three primitive types are supported — no objects, arrays, or nested structures. Lists are flat by design.
Required vs Optional Fields
Section titled “Required vs Optional Fields”Fields without optional: true are required in every entry. Optional fields may be omitted or set to null. This enables provider-specific columns — etherscanAlias is optional because not every chain has an Etherscan explorer.
Dependencies Between Lists
Section titled “Dependencies Between Lists”Lists can declare dependencies on other lists using meta.dependsOn:
meta: { name: 'germanBundeslaender', version: '1.0.0', description: 'German federal states', fields: [ { key: 'name', type: 'string', description: 'State name' }, { key: 'code', type: 'string', description: 'State code' }, { key: 'countryRef', type: 'string', description: 'Reference to parent country' } ], dependsOn: [ { ref: 'isoCountryCodes', version: '1.0.0', condition: { field: 'alpha2', value: 'DE' } } ]}Dependency Rules
Section titled “Dependency Rules”refmust resolve — the referenced list name must exist in the registry- Version pinning —
versionpins the dependency to a specific semver version conditionis optional — when present, it filters the parent list- No circular dependencies — A depends on B means B cannot depend on A
- Maximum depth: 3 levels — prevents resolution complexity
flowchart TD A[isoCountryCodes] --> B[germanBundeslaender] A --> C[usStates] B --> D[germanLandkreise] D -.->|"FORBIDDEN: depth 4"| E[germanGemeinden]Referencing from Schemas
Section titled “Referencing from Schemas”Schemas reference shared lists in the main.sharedLists array:
main: { sharedLists: [ { ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } } ]}Reference Fields
Section titled “Reference Fields”| Field | Type | Required | Description |
|---|---|---|---|
ref | string | Yes | List name to reference |
version | string | Yes | Required list version |
filter | object | No | Filter entries before injection |
Filter Types
Section titled “Filter Types”| Filter | Description | Example |
|---|---|---|
{ key, exists: true } | Only entries where field exists and is not null | { key: 'etherscanAlias', exists: true } |
{ key, value } | Only entries where field equals value | { key: 'mainnet', value: true } |
{ key, in: [...] } | Only entries where field is in the list | { key: 'chainId', in: [1, 137, 42161] } |
When filter is omitted, all entries are injected.
Runtime Injection Lifecycle
Section titled “Runtime Injection Lifecycle”flowchart TD A[Schema declares sharedLists] --> B[Resolve list by name + version] B --> C{List found?} C -->|No| D[Load error: list not found] C -->|Yes| E{Version matches?} E -->|No| F[Load error: version mismatch] E -->|Yes| G[Apply filter] G --> H[Inject into sharedLists for factory] H --> I[Build reverse-lookup index]- Resolve — The runtime looks up each
refin the list registry. If not found, the schema fails to load. - Version Check — The runtime verifies the registry version matches. A mismatch is a hard error.
- Filter — If a
filteris declared, the runtime applies it. Otherwise all entries pass through. - Inject — Filtered entries are passed to the
handlersfactory assharedLists:
export const handlers = ( { sharedLists } ) => ({ getGasOracle: { preRequest: async ( { struct, payload } ) => { const chain = sharedLists.evmChains .find( ( entry ) => entry.etherscanAlias === payload.chainName ) return { struct, payload } } }})List Registry
Section titled “List Registry”All shared lists are tracked in _lists/_registry.json:
{ "specVersion": "2.0.0", "lists": [ { "name": "evmChains", "version": "1.0.0", "file": "_lists/evm-chains.mjs", "entryCount": 15, "hash": "sha256:def456..." } ]}Registry Invariants
Section titled “Registry Invariants”- Every
.mjsfile in_lists/must have a registry entry - Every registry entry must point to an existing file
- The
hashmust match the current file content - Validate with
flowmcp validate-lists
File Conventions
Section titled “File Conventions”_lists/ _registry.json evm-chains.mjs fiat-currencies.mjs iso-country-codes.mjs- File names use kebab-case:
evm-chains.mjs - List names use camelCase:
evmChains - One list per file