Canonical chain configuration for Sourcify — the open-source smart contract verification service. The Sourcify server fetches sourcify-chains-default.json from this repository at startup.
sourcify-chains-default.json is auto-generated by a nightly CI job (and on every push to main) that:
- Fetches supported chains from external APIs: QuickNode, dRPC, Etherscan, and Blockscout
- Probes each QuickNode and dRPC chain to verify it is alive and detect trace support (see below)
- Merges with manually maintained config files in this repo
- Writes the merged result to
sourcify-chains-default.json
A chain is auto-included if it appears in any of:
- QuickNode console API — and is not dead (see probing below). The API reports
chain_id: nullfor every non-EVM network but also for some EVM networks (e.g. Robinhood, Monad, Ink, Soneium). Each null-chain-id network is asked for its chain id viaeth_chainIdon its QuickNode endpoint; networks that answer with a chain id are included, the rest are skipped (non-EVM, or not usable). This needsQUICKNODE_API_KEY+QUICKNODE_SUBDOMAIN; without them null-chain-id networks are skipped. - dRPC chains config (
https://raw.githubusercontent.com/drpcorg/public/main/chains.yaml,type: ethentries) — and is not dead (see probing below) - Etherscan chainlist API
- Blockscout's own hosted instances (where
hostedBy === "blockscout")
Every QuickNode and dRPC chain is probed on each run. Probing:
- Calls
eth_getBlockByNumber("latest")on the provider URL to get the current block number. If the provider returns an error (e.g. "Unknown network"), the chain is dead on that provider. - Verifies
eth_chainIdmatches the chain id the URL is mapped to (retried a few times). If the RPC reports a different chain id — a provider repointed a network slug to a renumbered chain — or ifeth_chainIdcan't be confirmed after retries, the chain is dead on that provider. Confirmed mismatches are written tochainid-mismatches.jsonand surfaced in the regenerate PR description. - Calls
eth_getCode(zero address,latest). Sourcify needseth_getCodeto fetch a contract's bytecode during verification, so an RPC that does not serve it is dead even if it answers blocks andeth_chainId(e.g. QuickNode's Hyperliquid endpoints return an empty HTTP 404 for it). - Looks up a cached transaction hash from
tx-cache.json(keyed by chain ID). If one exists, it is used directly and the block scan is skipped. Otherwise, scans blocks[latest-50 .. latest-550](a 500-block window) looking for a transaction. Skipping the most recent 50 blocks avoids transactions whose traces may not yet be indexed. If no transaction is found in that window, the chain is treated as inactive (dead). - If a transaction is found, calls
trace_transactionthendebug_traceTransactionon it to detect which trace method the provider supports for that chain.
Dead chains (step 1–4 failed) are excluded from the provider's RPC list and do not count as a discovery source. A chain left with no live RPC is kept in the output as supported: false (deprecated) so it stays discoverable, rather than being removed entirely. The same applies to a chain that drops out of every discovery source (e.g. a provider stops listing a sunset network): it is carried forward from the previous output as supported: false rather than hard-removed. Deprecation — not removal — is the terminal state for a chain that has been generated at least once.
Trace support result per provider:
| Value | Meaning |
|---|---|
"trace_transaction" |
Provider supports Parity-style tracing for this chain |
"debug_traceTransaction" |
Provider supports Geth-style tracing for this chain |
"none" |
Chain is alive but neither trace method is available |
null |
Chain is dead on this provider |
QuickNode and dRPC are probed independently — they may expose different trace methods for the same chain. Each probe has a 45 s total timeout (accommodates retries across individual RPC calls).
Routescan and third-party Blockscout instances do not qualify a chain for inclusion on their own — they only contribute fetchContractCreationTxUsing data. We also make use of the chainid.network/chains.json (a.k.a "chainlist") file to get the public RPCs of the chains if no paid provider RPCs are available.
Every blockscoutApi entry carries a proApi flag. It is true when the chain is listed in the Blockscout PRO API chains config, which is the list the PRO API gateway (api.blockscout.com) serves. The flag is also set on manual blockscoutApi overrides. Not every Blockscout-hosted instance is in that list, so hostedBy === "blockscout" alone is not enough to use the PRO API. Entries with proApi: false must use the instance url directly.
Beyond auto-discovery, chains can also be manually included or excluded:
additional-chains.json— manually include chains that are not auto-discovereddeprecated-chains.json— manually exclude chains, even if they appear in provider APIschain-overrides.json— override or extend fields for any included chain (custom RPCs, trace support, etc.)
Override or extend fields for any chain. A chain-overrides entry alone is sufficient to include a chain in the output — it does not need to be auto-discovered first. Use this for chains that need non-default configuration (e.g. custom RPCs, trace support, custom fetchContractCreationTxUsing).
{
"1": {
"sourcifyName": "Ethereum Mainnet",
"fetchContractCreationTxUsing": { "avalancheApi": true },
"rpc": [
{
"type": "FetchRequest",
"url": "https://rpc.mainnet.ethpandaops.io",
"headers": [
{ "headerName": "CF-Access-Client-Id", "headerEnvName": "CF_ACCESS_CLIENT_ID" },
{ "headerName": "CF-Access-Client-Secret", "headerEnvName": "CF_ACCESS_CLIENT_SECRET" }
]
}
],
"hidden": true
}
}Allowed fields per entry:
| Field | Description |
|---|---|
sourcifyName |
Display name (overrides chainid.network name) |
etherscanApi |
Opt into Etherscan-compatible explorer support for a chain not in the Etherscan registry: { "supported": true, "url": "<base-url>", "apiKeyEnvName"?: "<ENV>" }. apiKeyEnvName is optional — omit it for explorers that don't require an API key. |
fetchContractCreationTxUsing |
Additional fetch methods (blockscoutApi, avalancheApi, etc.). A blockscoutApi override needs only the url; the proApi flag is set by the generator. |
rpc |
RPCs defined here have higher priority than auto-discovered provider RPCs |
hidden |
If true, the chain is omitted from GET /chains and from GET /v2/contract/all-chains/:address on the Sourcify server. Verification and explicit per-chainId lookups still work. |
Chains that are not auto-discovered but are still supported by Sourcify. Each entry needs only a sourcifyName:
{
"57": { "sourcifyName": "Syscoin Mainnet" },
"82": { "sourcifyName": "Meter Mainnet" }
}The generator injects supported: true and discoveredBy: ["additional-chains"] at output time. This file is intentionally minimal — only sourcifyName belongs here for easier human inspection. Typically the RPCs of these chains are imported from the chainid.network/chains.json (a.k.a "chainlist") file.
A chain must not appear in additional-chains.json if it is already auto-discovered — the generator will throw. If a chain becomes auto-discovered over time, remove it from additional-chains.json (and add a chain-overrides.json entry if a custom name or config is still needed). A chain must also not appear in both additional-chains.json and deprecated-chains.json (the generator will throw).
Chains that should be excluded from the output entirely, even if they appear in provider APIs or are auto-discovered. These will have "supported": false in the output:
{
"3": "Ethereum Ropsten Testnet",
"4": "Ethereum Rinkeby Testnet"
}Chains whose dRPC endpoint is too unreliable to probe or use. These chains are excluded from dRPC probing entirely and will not get a dRPC RPC entry in the output, even if they appear in the dRPC chain list. They may still be discovered and served via other providers (QuickNode, Etherscan, Blockscout):
{
"1284": "Moonbeam",
"1313161555": "Aurora Testnet"
}Chain IDs the Avalanche Glacier API lists for an unrelated chain — usually an ephemeral AvaCloud test subnet whose chain ID collides with a different chain in our registry (e.g. Glacier serves 4114 as the test subnet WT0615TP, but 4114 is Citrea Mainnet here). These chains are excluded from the Glacier match, so fetchContractCreationTxUsing.avalancheApi: true is not set on the wrong chain:
{
"2201": "Stable Testnet",
"4114": "Citrea Mainnet"
}A cache of known transaction hashes, keyed by chain ID. When a cached hash is available for a chain, the probe skips the 500-block scan entirely and uses it directly. This prevents false "chain inactive" results for low-activity chains where no transaction happens to appear in the scan window.
The cache is updated automatically after each generation run and committed alongside sourcify-chains-default.json. It only caches the tx hash — trace probe results are never cached; those are always re-probed fresh each run.
{
"1": "0xabc123...",
"137": "0xdef456..."
}Maps chain IDs to the environment variable name holding their Etherscan API key. Chains not listed here fall back to ETHERSCAN_API_KEY:
{
"1": "ETHERSCAN_API_KEY_MAINNET",
"56": "ETHERSCAN_API_KEY_BSC",
"137": "ETHERSCAN_API_KEY_POLYGON"
}Do not edit this file directly — it is auto-generated by the CI pipeline.
Each entry includes:
| Field | Description |
|---|---|
sourcifyName |
Human-readable chain name |
supported |
true for active chains; false for deprecated chains |
discoveredBy |
Which sources caused this chain to be included (e.g. ["quicknode", "drpc", "etherscan"]) |
rpc |
Ordered list: override RPCs → dRPC (if alive) → QuickNode (if alive) → public (chainid.network) |
etherscanApi |
Etherscan API config, if the chain is on Etherscan's chainlist |
fetchContractCreationTxUsing |
APIs used to look up contract creation transactions |
Example output entry for Ethereum Mainnet:
{
"sourcifyName": "Ethereum Mainnet",
"supported": true,
"discoveredBy": ["quicknode", "drpc", "etherscan", "blockscout", "chain-overrides"],
"fetchContractCreationTxUsing": {
"avalancheApi": true,
"etherscanApi": true,
"blockscoutApi": { "url": "https://eth.blockscout.com/", "proApi": true }
},
"etherscanApi": { "supported": true, "apiKeyEnvName": "ETHERSCAN_API_KEY_MAINNET" },
"rpc": [
{ "type": "FetchRequest", "url": "https://rpc.mainnet.ethpandaops.io", "headers": [...] },
{ "type": "APIKeyRPC", "url": "https://lb.drpc.live/ethereum/{API_KEY}", "apiKeyEnvName": "DRPC_API_KEY", "traceSupport": "trace_transaction" },
{ "type": "APIKeyRPC", "url": "https://{SUBDOMAIN}.quiknode.pro/{API_KEY}/", "apiKeyEnvName": "QUICKNODE_API_KEY", "subDomainEnvName": "QUICKNODE_SUBDOMAIN", "traceSupport": "trace_transaction" }
]
}For each chain, the generator builds the RPC list in this priority order:
- Override RPCs from
chain-overrides.json(e.g. ethpandaopsFetchRequestentries) - dRPC
APIKeyRPC(if the chain is on dRPC) — preferred due to lower cost - QuickNode
APIKeyRPC(if the chain is on QuickNode) - Public RPCs from chainid.network — only if none of the above exist
Wait for the next nightly run — it will be picked up automatically. If you need to override a field (e.g. add a custom RPC or fetchContractCreationTxUsing), open a PR adding an entry to chain-overrides.json.
If the chain needs only a name and no custom config, add it to additional-chains.json:
{
"12345": { "sourcifyName": "My Chain Mainnet" }
}If the chain also needs custom RPCs or other config, add it to chain-overrides.json instead — a chain-overrides entry alone is sufficient to include a chain.
When opening the PR, name the branch add-chain-<chainId> (e.g. add-chain-42429) so the Test New Chain workflow runs the integration test for just that chain. For multiple chains in one PR, separate IDs with - (e.g. add-chain-1111-2222-3333 → tests chains 1111,2222,3333). PRs on branches without the prefix don't trigger this workflow.
Add the chain ID and name to deprecated-chains.json. If the chain is also in additional-chains.json, remove it from there too.
Add an entry to etherscan-api-keys.json mapping the chain ID to the env var name:
{
"12345": "ETHERSCAN_API_KEY_MYCHAIN"
}External APIs are flaky — dRPC sometimes drops chains, Routescan is inconsistent, trace probing returns different results across runs. Without filtering, every flake produces a noisy PR with spurious removals that revert on the next run.
scripts/sync.ts sits between generate.ts and the PR step. It applies a consecutive-run threshold before including reductive changes:
| Change type | Threshold |
|---|---|
| New chain, new RPC, new traceSupport, new fetchUsing key, new etherscanApi, new discoveredBy source | Immediate (1 run) |
| Chain removed, RPC removed, traceSupport changed/removed, fetchUsing key removed or value changed, etherscanApi removed, discoveredBy source removed | 5 consecutive runs |
RPC removed from chain-overrides.json by hand |
Immediate (1 run) |
If a reductive change disappears between runs (API flake recovered), its counter resets and it is not included.
An RPC that was removed from chain-overrides.json by hand is not an API flake, so sync.ts removes it right away. To detect this, the state records the override RPC URLs listed in chain-overrides.json on the last run (overrideRpcs). A URL that is no longer listed is kept in removedOverrideRpcs until the committed file no longer contains it, so the removal stays immediate on every run until the regenerate PR is merged. If another source still provides the URL (for example a public RPC from chainid.network), it is not recorded and stays a regular RPC. An override RPC that is still listed but fails its liveness probe keeps the threshold. On the first run after this state was introduced, the URLs are only recorded, so earlier removals keep the threshold.
State is persisted in change-history.json on an orphan branch chain-sync-state. The workflow fetches it at the start of each run and pushes the updated counters back at the end.
Output: sync.ts overwrites sourcify-chains-default.json with the stabilized result (snapshot with unstable removals reverted to their baseline values) and writes pr-description.txt summarising what is immediately included, what is newly stabilised, and what is still pending.
After each merge to main that changes sourcify-chains-default.json, the release workflow (.github/workflows/release.yml) uploads the file to Google Cloud Storage. The canonical fetch URL for Sourcify consumers is:
https://storage.googleapis.com/sourcifyeth-chains/sourcify-chains-default.json
The GitHub raw URL (https://raw.githubusercontent.com/sourcifyeth/sourcify-chains/main/sourcify-chains-default.json) remains available as a fallback but GCS is preferred for uptime.
CORS is configured at the bucket level to allow GET/HEAD from any origin, so browser clients can fetch the file directly.
The generation workflow (.github/workflows/generate.yml) runs:
- On push to
mainwhenchain-overrides.json,additional-chains.json,deprecated-chains.json,etherscan-api-keys.json, orscripts/**change - Twice daily at 02:00 and 14:00 UTC (to pick up new chains from provider APIs and accumulate stability-filter counts)
- Manually via
workflow_dispatch
If sourcify-chains-default.json or tx-cache.json changes after stability filtering, the bot opens a PR from a fixed branch chore/regenerate-chains targeting main. If a PR for that branch is already open, it force-pushes the update to it instead of opening a new one. The PR body is generated from pr-description.txt and lists immediately included changes, newly stabilised removals, and still-pending changes. The PR must be reviewed and merged manually.
Required secrets:
| Secret | Purpose |
|---|---|
QUICKNODE_CONSOLE_API_KEY |
Fetches the list of chains QuickNode supports (Console API, separate from RPC key) |
QUICKNODE_API_KEY |
RPC key used to probe chain liveness and trace support on QuickNode |
QUICKNODE_SUBDOMAIN |
Subdomain for the QuickNode RPC endpoint |
DRPC_API_KEY |
RPC key used to probe chain liveness and trace support on dRPC |
npm install
QUICKNODE_CONSOLE_API_KEY=<key> \
QUICKNODE_API_KEY=<key> \
QUICKNODE_SUBDOMAIN=<subdomain> \
DRPC_API_KEY=<key> \
npm run generateQUICKNODE_CONSOLE_API_KEY is required to fetch the chain list. The RPC keys (QUICKNODE_API_KEY, QUICKNODE_SUBDOMAIN, DRPC_API_KEY) are optional — without them, probing is skipped and no traceSupport is set on provider RPCs.
The script probes all QuickNode and dRPC chains, writes sourcify-chains-default.json, and prints a summary.
tests/chain-tests.test.ts and tests/etherscan-instances.test.ts exercise the chain configs against a real Sourcify server running in Docker. Requires Node 22+ and Docker.
Copy the template and fill in your keys:
cp .secrets.dev .secretsDRPC_API_KEY and QUICKNODE_API_KEY are required (the server crashes on startup without them). ETHERSCAN_API_KEY is recommended; it is the fallback for every chain's Etherscan-family explorer.
Start postgres + sourcify, then run the tests from your shell:
# Load secrets into your shell so docker compose can interpolate them
set -a; source .secrets; set +a
# Start the stack
docker compose -f tests/docker-compose.yml up -d postgres
bash tests/migrate.sh
docker compose -f tests/docker-compose.yml up -d sourcify
# Run all chains (slow: ~30 min) or scope to one
npm run test:integration
NEW_CHAIN_ID=1 npm run test:integration:chain # smoke test against Ethereum mainnet
# Clean up
docker compose -f tests/docker-compose.yml down -v2b. Run via act (matches CI)
Runs the actual integration-tests.yml workflow locally in a containerized GitHub Actions runner. Slower but verifies the workflow itself.
act workflow_dispatch \
-W .github/workflows/integration-tests.yml \
-j integration \
--container-architecture linux/amd64 \
-P ubuntu-latest=catthehacker/ubuntu:act-latest \
--bind \
--secret-file .secrets \
--env SOURCIFY_BASE_URL=http://host.docker.internal:5555Notes:
- On macOS, enable Settings → Advanced → "Allow the default Docker socket to be used" in Docker Desktop so act can find
/var/run/docker.sock. - The
--env SOURCIFY_BASE_URL=http://host.docker.internal:5555is macOS-specific: Docker Desktop's VM doesn't share a network namespace with the act runner, solocalhostfrom inside the runner can't reach the sourcify container. On Linux this flag isn't needed. - The act run does
npm ciinside a Linux container against the bind-mountednode_modules, overwriting platform-specific binaries (e.g.esbuild). After an act run,npm installon the host to restore yourdarwin-arm64/darwin-x64binaries before invoking other npm scripts.