/config/projects/providers.llms.txt
Providers & vendors
One API key, every chain. Instead of wiring one upstream per (vendor, network) pair, you declare a single provider and eRPC lazy-generates concrete upstreams the moment a new network is first requested. 23 vendor integrations are built in — from general-purpose node providers to ERC-4337 bundlers to public-endpoint catalogs — and a project with no config at all gets a sensible zero-cost default automatically.
What you get
- One line to cover all chains a vendor supports — no manual network list.
- Hot-path chain discovery with no I/O: static maps and lock-free remote caches keep routing decisions off the slow path.
- Zero-config fallback: eRPC auto-injects
repository+envioproviders when no upstreams or providers are configured.
Quick taste
Illustrative, not a tuned production config — one key, every Alchemy chain:
projects: - id: main upstreams: - # URL shorthand — one line, all Alchemy chains endpoint: alchemy://YOUR_API_KEYAgent reference
Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:
Prompt Example #1: set up providers from scratch
I want to configure eRPC providers so a single API key per vendor covers all EVM chains automatically. Add Alchemy and dRPC as providers, restrict Alchemy to the chains we actually need (mainnet, Arbitrum, Base, Polygon), and leave everything else to dRPC. Work with my existing eRPC config. Read the full reference first: https://docs.erpc.cloud/config/projects/providers.llms.txt
Prompt Example #2: add per-network overrides to an existing provider
My eRPC config already has providers for Alchemy and QuickNode. I need Arbitrum (evm:42161) to pre-split eth_getLogs at 1000 blocks and have a lower score multiplier for QuickNode on mainnet. Apply these as provider overrides without touching the upstreams list. Reference: https://docs.erpc.cloud/config/projects/providers.llms.txt
Prompt Example #3: debug a provider not generating upstreams on cold start
My eRPC instance logs ErrNetworkInitializing for evm:130 on every first request after a redeploy, even though I have a conduit provider configured. Explain why conduit returns ErrRemoteCacheCold on cold start and what I can pair it with to avoid the first-request delay. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/projects/providers.llms.txt
Prompt Example #4: route ERC-4337 bundler traffic to pimlico
Add Pimlico as a provider in my eRPC config so all bundler methods (eth_sendUserOperation, pimlico_*, pm_*) route to Pimlico while general eth_* methods still hit Alchemy. Explain the method allow-list that Pimlico injects automatically. Reference: https://docs.erpc.cloud/config/projects/providers.llms.txt
Prompt Example #5: split QuickNode endpoint groups by tag
I have two logical QuickNode endpoint groups: one tagged "archive" and one tagged "realtime". Wire them as separate providers in my eRPC config so each group's endpoints are used for the right traffic, and explain the apiKey cache-key footgun so I don't accidentally share snapshots. Reference: https://docs.erpc.cloud/config/projects/providers.llms.txt
Providers & vendors — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
Two distinct concepts. A vendor (common.Vendor interface) is stateless integration logic: Name(), OwnsUpstream(cfg), SupportsNetwork(ctx, logger, settings, networkId), GenerateConfigs(...), and GetVendorSpecificErrorIfAny(...). A provider is a configured instance of a vendor: ProviderConfig{Id, Vendor, Settings, OnlyNetworks, IgnoreNetworks, UpstreamIdTemplate, Overrides}. All 23 vendors are registered in a global VendorsRegistry at startup; a per-project ProvidersRegistry resolves each providers[].vendor string and fails startup if the name is unknown. thirdparty/vendors_registry.go:L9-36 thirdparty/providers_registry.go:L15-34
Lazy upstream generation. When a request arrives for a network with no prepared upstreams, UpstreamsRegistry.PrepareUpstreamsForNetwork schedules — exactly once per network via sync.Once — one bootstrap task per declared provider named network/<networkId>/provider/<providerId>. Each task: (1) calls Provider.SupportsNetwork, applying ignoreNetworks deny-list first, then onlyNetworks allow-list, then delegating to the vendor; (2) calls Provider.GenerateUpstreamConfigs, which copies the first wildcard-matching overrides entry (or creates a fresh defaults-filled config), stamps VendorName, renders the upstream id from upstreamIdTemplate, and runs os.ExpandEnv over every generated endpoint; (3) registers the resulting configs as ordinary upstream bootstrap tasks. The caller waits up to 30 seconds (polling every 200 ms) for at least one upstream to become ready. While tasks run, clients receive the retryable ErrNetworkInitializing; if all tasks finish without producing any upstreams, the error becomes the retryable ErrNetworkNotSupported. Failed tasks auto-retry with exponential backoff (factor 1.5, 3 s → 130 s). upstream/registry.go:L155-293
Vendor chain-support strategies. Vendors answer "do you support this network?" in four ways:
- Static in-code map (ankr, blastapi, blockdaemon, blockpi, dwellir, infura, llama, onfinality, etherspot): instant, no I/O, works on cold start.
- Remote catalog with static fallback (alchemy — 134-chain map; drpc — 150-chain map): fetches on warm paths; cold start falls back to the built-in map so the first request never blocks on I/O.
- Remote catalog, no fallback (conduit, superchain, tenderly, quicknode, chainstack, repository): cold start returns the retryable sentinel
ErrRemoteCacheCold; the initializer retries with 3 s → 130 s exponential backoff until the first fetch lands. - Live
eth_chainIdprobe (envio for unknown chains, erpc, pimlico for unknown chains, routemesh, thirdweb): sends a real JSON-RPC call to the candidate endpoint with a 10 s timeout; probe clients are cached per chain id insync.Map.
The request-path safety rule. Any vendor consulting remote data on the hot path uses the shared RemoteDataCache[T]: reads are a single atomic.Pointer load — never a mutex, never I/O. Staleness triggers a single-flight async refresh in a goroutine with its own 90 s timeout, fully decoupled from the caller's context. thirdparty/remote_cache.go:L13-78
URL shorthand scheme. Any upstream whose endpoint scheme is not http, https, grpc, or grpc+bds is converted to a ProviderConfig at config-defaults time. The evm+ prefix is stripped first (evm+alchemy:// equals alchemy://). The original upstream config (failsafe, rate limits, method filters, etc.) is placed into overrides: {"*": ...} so it applies to every generated upstream. The provider id defaults to <vendor>-<n> (process-wide counter per vendor name). Unknown scheme is a config-load error. common/defaults.go:L1113-1241
Default providers. A project with zero upstreams, zero providers, and no CLI --endpoint flags automatically gets {id:"public", vendor:"repository"} + {id:"envio", vendor:"envio"} injected with all defaults, and logs a warning. Setting providers: [] explicitly still satisfies the zero-providers condition — the injection fires unless you have at least one upstream or one CLI endpoint. common/defaults.go:L1113-1146
Vendors also serve statically-configured upstreams. At NewUpstream, LookupByUpstream finds a vendor by explicit upstream.vendorName (exact match, wins — if the name is unknown, OwnsUpstream is NOT consulted and the result is nil) or the first vendor whose OwnsUpstream matches the endpoint (in registration order). If no vendor matches, vendorName is guessed as unknown-<rootDomain> from the endpoint host. The vendor's GenerateConfigs is invoked with settings == nil to apply vendor defaults (e.g. dRPC forcing autoIgnoreUnsupportedMethods: false, envio's method allow-list). Vendor-specific JSON-RPC error mapping fires before generic normalization: GetVendorSpecificErrorIfAny is called on the upstream's attached vendor first. architecture/evm/error_normalizer.go:L878-900 thirdparty/vendors_registry.go:L54-71
Config schema
projects[].providers[] — ProviderConfig
| field | type | default | behavior / footguns |
|---|---|---|---|
id | string | vendor name (p.Id = p.Vendor) | Unique handle; appears in <PROVIDER> placeholder and bootstrap task names network/<networkId>/provider/<id>. Shorthand-converted providers get <vendor>-<n> (process-wide counter). No duplicate-id check — two providers with the same default id both run and may produce upstream-id collisions. common/defaults.go:L1474-1476 util/ids.go:L32-41 |
vendor | string | — (required) | Must exactly match one of 23 registered Name() strings: goldsky, alchemy, blastapi, conduit, drpc, dwellir, envio, etherspot, infura, pimlico, quicknode, llama, thirdweb, repository, superchain, tenderly, chainstack, onfinality, erpc, blockpi, ankr, routemesh, blockdaemon. Unknown vendor → startup error. thirdparty/vendors_registry.go:L12-34 |
settings | map[string]any (VendorSettings) | nil | Free-form, vendor-interpreted. Redacted in JSON/YAML marshaling (never echoed in admin dump). See per-vendor tables below. common/config.go:L667-700 |
settings.creditUnits | map[string]int64 | nil | Vendor-generic (works for every vendor): per-method credit-unit overrides for the cost accounting behind X-ERPC-Credits (server.costHeaders). Merged per method over the vendor's built-in CreditUnitsProvider table (alchemy and quicknode ship their public pricing); "*" = the vendor's fallback for unlisted methods. Vendors with no table default to a flat 1 credit per request — set "*": 0 to opt a vendor out entirely. Overriding one method keeps the rest; copied onto every upstream the provider generates. thirdparty/provider.go:L59-77 |
onlyNetworks | []string | nil | Exhaustive allow-list (evm:<positiveInt> format, validated). If set, vendor's own SupportsNetwork is not called. Mutually exclusive with ignoreNetworks (validation error). thirdparty/provider.go:L42-49 |
ignoreNetworks | []string | nil | Exact-match deny-list checked before onlyNetworks and before vendor. Same evm:<positiveInt> validation. Omitted from MarshalJSON (present in YAML marshal). thirdparty/provider.go:L34-40 |
upstreamIdTemplate | string | "<PROVIDER>-<NETWORK>" | Placeholders: <VENDOR>, <PROVIDER>, <NETWORK> (full network id e.g. evm:1), <EVM_CHAIN_ID> (numeric; N/A for non-evm networks). Validation requires non-empty. common/defaults.go:L1477-1479 |
overrides | map[string]*UpstreamConfig | nil | Keys are wildcard patterns (WildcardMatch grammar: *, ?, | OR, & AND, ! NOT). First match from Go map iteration (nondeterministic for overlapping keys) is copied as the base config; no match → fresh UpstreamConfig with defaults. Pattern-match errors silently skipped. Each override validated with endpoint check skipped. thirdparty/provider.go:L77-101 |
URL shorthand derivation
| shorthand | derived settings |
|---|---|
goldsky://<secret>[?tier=&secret=] | {secret: authority} + tier/secret from query (host is always edge.goldsky.com) |
alchemy://<key> | {apiKey: key} |
ankr://<key> | {apiKey: key} |
blastapi://<key> | {apiKey: key} |
satelink://free@polygon | free tier, no key |
satelink://<key>@polygon | {apiKey: key} |
blockdaemon://<key> | {apiKey: key} |
blockpi://<key> | {apiKey: key} |
chainstack://<key>[?project=®ion=&…] | {apiKey: key} + all query params copied verbatim |
conduit://<key> | {apiKey: key} |
drpc://<key> | {apiKey: key} |
dwellir://<key> | {apiKey: key} |
envio://<rootDomain> | {rootDomain: host} |
erpc://<host>[/<path>][?secret=] | {endpoint: "https://"+host+"/"+path} + secret from query |
etherspot://<key> | {apiKey: key} |
infura://<key> | {apiKey: key} |
llama://<key> | {apiKey: key} |
onfinality://<key> | {apiKey: key} |
pimlico://<key> | {apiKey: key} |
quicknode://<key>[?tagIds=1,2&tagLabels=a,b] | {apiKey: key} + parsed tagIds/tagLabels |
repository://<host>/<path>[?query] | {repositoryUrl: "https://"+host+"/"+path+"?"+query} |
routemesh://<host>/rpc/<chainId>/<apiKey> | {baseURL: host, apiKey: pathParts[2]} — path must match exactly |
superchain://<spec> | {registryUrl: host+path} |
tenderly://<key> | {apiKey: key} |
thirdweb://<clientId> | {clientId: host} |
| unknown scheme | config-load error: unsupported vendor name in vendor.settings: <scheme> |
Vendor catalog
goldsky
Goldsky Edge (opens in a new tab) — a multi-region public RPC service built on eRPC. One secret token covers every chain Goldsky serves; upstreams are lazy-generated per network. SupportsNetwork probes live eth_chainId against https://edge.goldsky.com/{tier}/evm/{chainId}?secret={secret} (10 s timeout) and confirms the returned chain id matches — so the secret's plan authoritatively decides chain support. A missing secret makes SupportsNetwork return (false, nil) (silent skip, no retry storm). tier defaults to standard; endpoint defaults to https://edge.goldsky.com and may be overridden for enterprise/custom deployments (an explicit path on the base URL is respected verbatim and the tier route is not injected). The secret can also be supplied out-of-band via the X-ERPC-Secret-Token header on a preset endpoint. Probe clients are cached per "{url}-{chainId}". OwnsUpstream: goldsky:///evm+goldsky:// prefix, vendorName=="goldsky", or endpoint contains edge.goldsky.com.
| key | required | default |
|---|---|---|
secret | yes (auth fails without it) | — |
tier | no | standard |
endpoint | no | https://edge.goldsky.com |
thirdparty/goldsky.go:L31-249alchemy
Chain discovery: remote catalog (default https://app-api.alchemy.com/trpc/config.getNetworkConfig) keyed by URL via RemoteDataCache; cold-start fallback to a 134-chain built-in map; after a successful fetch the API result is merged with static defaults (API wins on conflict, static fills gaps). URL template: https://{subdomain}.g.alchemy.com/v2/{apiKey}. Error mapping: -32600 + authenticated message → ErrEndpointUnauthorized; code 3 → EVM revert.
| key | required | default |
|---|---|---|
apiKey | yes | — |
chainsUrl | no | https://app-api.alchemy.com/trpc/config.getNetworkConfig |
recheckInterval | no | 24h |
creditUnits | no | Alchemy's documented CU table |
Cost accounting: implements CreditUnitsProvider with Alchemy's documented compute-unit costs (opens in a new tab) (eth_call 26, eth_getLogs 60, eth_blockNumber 10, "*" 20, …). The table is auto-fetched from the docs' Markdown endpoint (compute-unit-costs.md) and cached ~1 week (refreshed off the hot path from SupportsNetwork/GenerateConfigs), so pricing tracks Alchemy without a redeploy; the built-in table is the cold-start / fetch-failure fallback. settings.creditUnits overrides per method. thirdparty/alchemy.go:L186-320
OwnsUpstream: alchemy:///evm+alchemy:// prefix, vendorName=="alchemy", or endpoint contains .alchemy.com/.alchemyapi.io. thirdparty/alchemy.go:L156-326
ankr
Static map of 47 chains. URL: https://rpc.ankr.com/{netName}/{apiKey} (URL-escaped). OwnsUpstream: scheme prefixes or rpc.ankr.com.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/ankr.go:L15-144blastapi
Static map of 79 chains. URL: https://{netName}.blastapi.io/{apiKey}; Avalanche appends /ext/bc/C/rpc. OwnsUpstream: scheme prefixes or .blastapi.io.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/blastapi.go:L15-181blockdaemon
Static map of 18 chains with heterogeneous path shapes. Auth is always header-based: apiKey is injected as Authorization: Bearer <apiKey> on every request regardless of whether the endpoint was preset or generated — a static upstream pointing at svc.blockdaemon.com fails at NewUpstream without apiKey. HTTP 401 → ErrEndpointUnauthorized.
| key | required | default |
|---|---|---|
apiKey | yes (even with preset endpoint) | — |
thirdparty/blockdaemon.go:L24-156blockpi
Static map of 56 chains (exported BlockPiNetworkNames). URL: https://{netName}.blockpi.network/v1/rpc/{escapedKey}; chain movement uses /rpc/v1/{escapedKey}/v1. Error mapping: message containing apikey is on another chain → ErrEndpointUnauthorized. OwnsUpstream: scheme prefixes or .blockpi.network.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/blockpi.go:L15-173chainstack
Account-level node discovery: paginated GET https://api.chainstack.com/v1/nodes/ with Authorization: Bearer {apiKey} and optional filter params; chain ids discovered by eth_chainId probe per node (concurrency-limited, semaphore 10, 10 s client timeout). Cache key includes all filter values so providers sharing an apiKey but different filters get separate snapshots. Missing apiKey → (false, nil) (silent skip). Cold cache → ErrRemoteCacheCold. GenerateConfigs fans out one upstream per matching running node; upstream endpoint = https_endpoint + "/" + auth_key; id suffix -<nodeID>. Node decode is intentionally lenient: each node is decoded individually from the paginated response and malformed nodes (type mismatches, missing fields) are skipped rather than failing the entire refresh. OwnsUpstream: scheme prefixes or .core.chainstack.com.
| key | required | default |
|---|---|---|
apiKey | yes | — |
recheckInterval | no | 1h |
project | no | "" |
organization | no | "" |
region | no | "" |
provider | no | "" |
type | no | "" |
thirdparty/chainstack.go:L51-429conduit
Dynamic-only discovery from networksUrl; no static fallback → cold start returns ErrRemoteCacheCold. URL: {httpEndpoint}/{apiKey}. OwnsUpstream: scheme prefixes or vendorName=="conduit" only (no domain heuristic — chains live on customer domains). Preset endpoint bypasses discovery entirely. Error mapping: -32600 + auth/api-key message → ErrEndpointUnauthorized; message "limit exceeded"/"capacity limit" → ErrEndpointCapacityExceeded; code in range [-32099, -32000] → ErrEndpointServerSideException (note: as written the condition code >= -32000 && code <= -32099 is never true — same impossible-range pattern as alchemy, so this branch is dead code).
| key | required | default |
|---|---|---|
apiKey | yes | — |
networksUrl | no | https://api.conduit.xyz/public/network/all |
recheckInterval | no | 24h |
thirdparty/conduit.go:L16-222drpc
Remote catalog (default https://lb.drpc.org/networks) with a 150-chain static fallback; after a successful fetch the fallback is fully replaced (no merge). Forces autoIgnoreUnsupportedMethods: false on every config (even preset endpoints) because dRPC transiently returns method-not-found responses that must stay retryable. URL: https://lb.drpc.org/ogrpc?network={name}&dkey={apiKey}. Catalog filters to premium EVM JSON-RPC chains only (blockchain_type=="eth" && api_type=="jsonrpc" && has_premium); when multiple names map to one chain id, the highest priority value wins. Error mapping: message "token is invalid" → ErrEndpointUnauthorized; "ChainException: Unexpected error (code=40000)" or "invalid block range" → ErrEndpointMissingData.
| key | required | default |
|---|---|---|
apiKey | yes | — |
chainsUrl | no | https://lb.drpc.org/networks |
recheckInterval | no | 24h |
creditUnits | no | dRPC's flat CU table |
Cost accounting: implements CreditUnitsProvider with dRPC's documented compute-unit costs (opens in a new tab) — a flat 20 CU for every billable method (debug/trace included, no multiplier tier), 0 for informational methods (eth_chainId, net_version, web3_clientVersion, …), and "*" = 20 for anything unlisted; settings.creditUnits overrides per method. thirdparty/drpc.go:L207-240
thirdparty/drpc.go:L19-455dwellir
Static map of 71 chains → subdomain prefixes. URL: https://{subdomain}.dwellir.com/{apiKey}. OwnsUpstream: only dwellir:// prefix (NOT evm+dwellir://) or .dwellir.com — the evm+ form still works because shorthand conversion runs first. Avalanche appends /ext/bc/C/rpc to the URL. SupportsNetwork quirk: an unparseable numeric chain id part returns (false, nil) rather than an error (unlike every other static-map vendor which propagates the parse error).
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/dwellir.go:L15-180envio
Method allow-list injected on every upstream (even preset static ones): ignoreMethods: ["*"] + 14 read-oriented methods (eth_getLogs, eth_blockNumber, eth_getBlockByNumber, etc.). SupportsNetwork: 61 known chain ids short-circuit true; unknown chains are live-probed via eth_chainId (10 s timeout; TLS error with "failed to verify certificate" → false). URL: https://{chainId}.{rootDomain}[/{apiKey}].
| key | required | default |
|---|---|---|
rootDomain | no | rpc.hypersync.xyz |
apiKey | no | "" |
thirdparty/envio.go:L20-258erpc
Chains to another eRPC instance. SupportsNetwork probes live eth_chainId. parseEndpointURL appends /{chainId} and optionally sets ?secret=. Port-based scheme inference applies in settings.endpoint; the shorthand erpc://host:8080 forces HTTPS. Probe clients are cached per "{url}-{chainId}".
| key | required | default |
|---|---|---|
endpoint | yes | — |
secret | no | "" |
thirdparty/erpc.go:L35-265etherspot
ERC-4337 bundler (Skandha). Method allow-list injected: ignoreMethods: ["*"] + 6 bundler methods (skandha_*, eth_sendUserOperation, etc.). Static map of 15 mainnets + 11 testnets. URL shape differs by network type: mainnet https://{networkName}-bundler.etherspot.io/, testnet https://testnet-rpc.etherspot.io/v1/{chainId}; ?apikey= appended unless apiKey is "public" or empty.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/etherspot.go:L15-146infura
Static map of 31 chains. URL: https://{netName}.infura.io/v3/{apiKey}. Error mapping: -32600 auth message → unauthorized; -32001/-32004 → unsupported; -32005 → capacity exceeded.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/infura.go:L15-163llama
Static map of 6 chains (eth, arbitrum, base, binance, optimism, polygon). URL: https://{netName}.llamarpc.com/{apiKey}. OwnsUpstream matches .llamarpc.com domain only (no llama:// scheme check — shorthand still works because conversion runs first). Cloudflare rate-limit message (code: 1015) → ErrEndpointCapacityExceeded.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/llama.go:L14-97onfinality
Static map of 25 chains. URL: https://{netName}.api.onfinality.io/rpc?apikey={escapedKey} (apiKey as query param). OwnsUpstream: scheme prefixes or .api.onfinality.io.
| key | required | default |
|---|---|---|
apiKey | yes | — |
thirdparty/onfinality.go:L15-122pimlico
ERC-4337 bundler/paymaster. Method allow-list injected: ignoreMethods: ["*"] + 11 bundler/paymaster methods (pimlico_*, pm_*, eth_sendUserOperation, etc.). SupportsNetwork: 67-chain static set short-circuits true; unknown chains probe live eth_chainId — missing apiKey is an error (unlike quicknode/chainstack which return false, nil). URL: "public" → https://public.pimlico.io/v2/{chainId}/rpc; else https://api.pimlico.io/v2/{chainId}/rpc?apikey={apiKey}.
| key | required | default |
|---|---|---|
apiKey | yes (error if missing, even for SupportsNetwork) | — |
thirdparty/pimlico.go:L21-252quicknode
Account endpoint discovery: paginated GET https://api.quicknode.com/v0/endpoints?limit=100&offset=N with x-api-key header and optional tag filters; a response-level error field aborts the refresh; chain ids discovered by eth_chainId probe per endpoint (concurrency-limited, semaphore 10, 10 s timeout). Cache key is apiKey only — two providers sharing an apiKey but different tagIds/tagLabels share one endpoint snapshot. Missing apiKey → (false, nil). Cold cache → ErrRemoteCacheCold. GenerateConfigs fans out one upstream per matching endpoint; id {upstreamId}-{endpointID}. Full error mapping: -32614 or (eth_getLogs + message "limited to") → ErrEndpointRequestTooLarge; -32009/-32007 → capacity exceeded; -32612/-32613 → ErrEndpointUnsupported; message "failed to parse" → client-side, not retryable toward network; -32010 (tx cost exceeds gas limit) → client-side but network-retryable (per-client gas caps differ); -32602 + "cannot unmarshal hex string" → invalid-argument client-side, not retryable; message "UNAUTHORIZED" → unauthorized; code 3 → EVM revert. Note: the error handler builds a fresh local details map, so per-request details passed by the caller are not modified.
| key | required | default |
|---|---|---|
apiKey | yes | — |
recheckInterval | no | 1h |
tagIds | no | nil |
tagLabels | no | nil |
creditUnits | no | QuickNode's published credit model |
Cost accounting: implements CreditUnitsProvider. When the account apiKey is set, the account-accurate per-method table is auto-fetched from QuickNode's Admin API (GET /v0/api-credits/{chain} (opens in a new tab), x-api-key; the chain slug is resolved from the already-discovered endpoint list) and cached ~1 week off the hot path. The built-in published model (opens in a new tab) (base 20 on EVM, 2x debug/trace = 40, 4x trace replays = 80) is the fallback when no key is set or the fetch fails. settings.creditUnits overrides per method. thirdparty/quicknode.go:L28-120
thirdparty/quicknode.go:L44-452repository
Fetches repositoryUrl returning {"<chainId>": {"endpoints": [...]}}. No static fallback → ErrRemoteCacheCold on cold start. Defaults autoIgnoreUnsupportedMethods: true — public RPCs are inconsistent in method support. GenerateConfigs fans out one upstream per http endpoint (http/https prefix only; ws/wss skipped); ids embed RedactEndpoint(ep) (https#redacted=ab12c) so dashboards can distinguish endpoints without exposing them. Each generated upstream gets deep copies of Evm, JsonRpc, Failsafe (per-element), RateLimitAutoTune, and Tags from the base config, and inherits IgnoreMethods, AllowMethods, and RateLimitBudget. OwnsUpstream: only repository:///evm+repository:// scheme prefixes (no domain matching — generated upstreams have arbitrary third-party hosts). One half of the implicit default provider pair.
| key | required | default |
|---|---|---|
repositoryUrl | no | https://evm-public-endpoints.erpc.cloud |
recheckInterval | no | 1h |
thirdparty/repository.go:L17-232routemesh
No chain catalog — SupportsNetwork always probes live eth_chainId. Missing apiKey → error (provider task retries forever). URL: https://{baseURL}/rpc/{chainId}/{apiKey}. Shorthand requires exact path shape /rpc/<chainId>/<apiKey> or config-load error.
| key | required | default |
|---|---|---|
apiKey | yes (error if missing) | — |
baseURL | no | lb.routemes.sh |
thirdparty/routemesh.go:L20-167superchain
OP Superchain registry. parseSuperchainSpec accepts: github.com/org/repo (defaults branch main, file chainList.json), github.com/org/repo/branch, github.com/org/repo/branch/path/to/file.json, GitHub UI blob/tree URLs (the /blob/ or /tree/ segment is stripped), full http(s):// URLs passed through, or bare domain/path (https prepended). GitHub specs are rewritten to raw.githubusercontent.com. Registry JSON is an array of {chainId, rpc:[...]}; entries with chainId ≤ 0 or empty rpc list dropped; no static fallback → ErrRemoteCacheCold. GenerateConfigs fans out one upstream per rpc URL with id suffix -{index} (e.g. {id}-0, {id}-1, …). OwnsUpstream: scheme prefixes or vendorName=="superchain" only (no domain matching).
| key | required | default |
|---|---|---|
registryUrl | no | https://raw.githubusercontent.com/ethereum-optimism/superchain-registry/main/chainList.json |
recheckInterval | no | 24h |
thirdparty/superchain.go:L16-312tenderly
Remote catalog from hardcoded https://api.tenderly.co/api/v1/supported-networks (not overridable via settings — there is no chainsUrl-style override). No static fallback → ErrRemoteCacheCold. Catalog decodes chain_id + network_slugs; slug preference: node_rpc_slug > vnet_rpc_slug > explorer_slug; entries with no usable slug or unparseable chain id are skipped. URL: https://{slug}.gateway.tenderly.co/{apiKey}. OwnsUpstream: scheme prefixes, vendorName=="tenderly", or .gateway.tenderly.co.
| key | required | default |
|---|---|---|
apiKey | yes | — |
recheckInterval | no | 24h |
thirdparty/tenderly.go:L34-285thirdweb
No chain catalog — SupportsNetwork always probes live eth_chainId. Missing clientId → error. URL: https://{chainId}.rpc.thirdweb.com/{clientId}.
| key | required | default |
|---|---|---|
clientId | yes (error if missing) | — |
thirdparty/thirdweb.go:L35-141Worked examples
All patterns below are distilled from real production fleets; comments explain the non-obvious choices.
1. Multi-vendor setup with score de-weighting. Production fleets use Alchemy as the
premium primary and cheaper providers (dRPC, Envio) as volume fillers. The scoreMultipliers
overall factor deliberately de-weights weaker vendors so the scorer favors Alchemy on
latency-sensitive methods while still using them as backfill:
projects: - id: main providers: - id: alchemy vendor: alchemy settings: # premium vendor — no score penalty apiKey: ${ALCHEMY_KEY} # keep Alchemy away from chains where it adds no value or costs more ignoreNetworks: [evm:999] overrides: # catch-all: apply rate limit budget + light de-weight so fast vendors win "evm:*": rateLimitBudget: alchemy-global routing: scoreMultipliers: - network: "" method: "" # 0.2 = use as 20% of baseline score — Alchemy only wins when # others are slow, erroring, or behind on block head overall: 0.2 - id: drpc vendor: drpc settings: apiKey: ${DRPC_KEY} - id: envio vendor: envio settings: rootDomain: rpc.hypersync.xyz2. Per-chain getLogs splitting via provider overrides. High-throughput chains (Arbitrum,
Sei) routinely hit block-range limits when indexers issue large eth_getLogs queries.
Production sets getLogsAutoSplittingRangeThreshold per chain inside the provider's
overrides so every upstream that vendor auto-generates for that chain inherits the limit
without wiring individual upstreams:
projects: - id: main providers: - id: quicknode vendor: quicknode settings: apiKey: ${QN_KEY} ignoreNetworks: [evm:1, evm:8453, evm:42161] overrides: # catch-all: slow down state polling on every QN chain (it polls fast by default) "evm:*": evm: # 10s is a safe floor; most chains don't need sub-second head tracking statePollerInterval: 10s routing: scoreMultipliers: - network: "" method: "" # 0.3 = mid-tier weight; QN is good but not the fastest in tests overall: 0.3 # Arbitrum produces enormous logs — split at 1000 blocks before upstream rejects "evm:42161": evm: getLogsAutoSplittingRangeThreshold: 1000 # Sei has aggressive range limits — split at 500 "evm:1329": evm: getLogsAutoSplittingRangeThreshold: 5003. Provider-level method exclusions to avoid bad upstreams. Some vendors (dwellir,
blockpi) do not support trace_filter reliably on testnets. Production adds
ignoreMethods per-chain inside overrides so auto-generated upstreams for those
chains never receive trace requests that would 405-error and burn retry budget:
projects: - id: main providers: - id: dwellir vendor: dwellir settings: apiKey: ${DWELLIR_KEY} # skip chains covered by better vendors — avoids redundant SupportsNetwork probes ignoreNetworks: [evm:1, evm:137, evm:42161, evm:8453] overrides: # Sepolia and BSC testnet: trace_filter returns errors on dwellir — exclude it # so the scorer doesn't keep retrying it "evm:11155111": ignoreMethods: [trace_filter] "evm:97": ignoreMethods: [trace_filter]4. ERC-4337 bundler alongside general RPC. Pimlico and etherspot auto-inject method
allow-lists so only bundler methods route to them — combine with a general-purpose provider
for all other methods. No manual allowMethods needed; the vendor injects it at
NewUpstream time:
projects: - id: main upstreams: - endpoint: alchemy://${ALCHEMY_KEY} # pimlico auto-injects: ignoreMethods: ["*"] + allowMethods: [eth_sendUserOperation, # pimlico_*, pm_*] — bundler traffic routes here automatically - endpoint: pimlico://${PIMLICO_KEY}5. QuickNode with tag-based endpoint groups. Use separate apiKey values per tag
group — the RemoteDataCache is keyed by apiKey only, so two providers sharing one key
with different tagIds/tagLabels both see whichever snapshot the first refresh produced:
projects: - id: main providers: # archive-grade endpoints tagged in the QuickNode dashboard as "archive" - id: quicknode-archive vendor: quicknode settings: # must be a DIFFERENT apiKey than the realtime group — # same apiKey + different tagLabels still shares one endpoint snapshot apiKey: ${QN_ARCHIVE_KEY} tagLabels: archive # fast tip-of-chain endpoints on a separate key - id: quicknode-realtime vendor: quicknode settings: apiKey: ${QN_REALTIME_KEY} tagLabels: realtimeBest practices
- Use URL shorthands (
alchemy://KEY) for the common case — they are equivalent to a fullproviders[]entry with a"*"override and avoid boilerplate. - Use
onlyNetworksto restrict expensive vendors to the chains you actually need; it also prevents the vendor'sSupportsNetworkfrom being called for irrelevant chains. - Use exact
evm:<chainId>keys inoverrides— never mix a catch-all"*"with more-specific patterns in the same map. Go map iteration is nondeterministic; overlapping keys produce different results across restarts. - For QuickNode with multiple logical endpoint groups, use a separate
apiKeyper group (provisioned in the QuickNode dashboard). Sharing oneapiKeywith differenttagIds/tagLabelsshares one endpoint snapshot due to the apiKey-only cache key. recheckIntervalcannot be set from YAML or TypeScript config. The type assertion.(time.Duration)fails on YAML strings and JSON numbers. Use programmatic Go config (common.VendorSettings{"recheckInterval": 12 * time.Hour}) if you need a custom freshness window.- Vendors with no static fallback (conduit, superchain, tenderly, quicknode, chainstack, repository) return
ErrRemoteCacheColdon cold start. The initializer retries with backoff, but first requests for those networks will seeErrNetworkInitializinguntil the remote catalog fetches. Pair these vendors with a fallback provider that has a static map if cold-start latency matters. - Do not use
providers: []to "disable" auto-injection — an explicit empty providers list still triggers default injection if upstreams is also empty. Provide at least one upstream or one provider to take full control.
Edge cases & gotchas
recheckIntervalfrom YAML is silently ignored — every vendor reads it with.(time.Duration). YAML scalars decode tostring/int64so the assertion fails and the vendor default applies. Only Go programmatic config can set it.thirdparty/repository.go:L56-59- Cold-start contract differs by vendor: alchemy/drpc fall back to built-in maps; conduit/superchain/tenderly/quicknode/chainstack/repository return
ErrRemoteCacheColdand block until the first remote fetch lands.thirdparty/remote_cache.go:L85 - drpc replaces its fallback after a successful fetch (no merge); alchemy merges fetched data with its static defaults (API wins on conflict, static fills gaps).
thirdparty/drpc.go:L360-422thirdparty/alchemy.go:L386-405 - Missing-apiKey behavior is inconsistent: pimlico/routemesh/thirdweb
SupportsNetworkreturn an error (provider task retries forever); quicknode/chainstack return(false, nil)and are silently skipped.thirdparty/pimlico.go:L120-123 - blockdaemon requires
apiKeyeven with a preset endpoint — static upstreams pointing atsvc.blockdaemon.comfail atNewUpstreambecauseGenerateConfigsis called withsettings=nil.thirdparty/blockdaemon.go:L85-88 - Impossible range conditions in alchemy/conduit error mapping: alchemy's
code >= -32099 && code <= -32599can never be true; only the message-match and code-3 branches actually fire.thirdparty/alchemy.go:L275-326 - infura's Avalanche
/ext/bc/C/rpcbranch is unreachable — map keys areavalanche-mainnet/avalanche-fujibut branch testsava-mainnet/ava-testnet.thirdparty/infura.go:L90-93 overridesfirst-match is Go map-iteration order (nondeterministic for overlapping patterns). Pattern-match errors are silently skipped.thirdparty/provider.go:L81-91- quicknode cache key is
apiKeyonly — two providers with the same apiKey but differenttagIds/tagLabelsshare one endpoint snapshot. Chainstack includes filters in its cache key and is safe.thirdparty/quicknode.go:L184-205 erpc://host:8080shorthand forces HTTPS —buildProviderSettingshardcodeshttps://. Useproviders[].settings.endpointwith an explicit URL for non-TLS targets.common/defaults.go:L1389-1404onlyNetworks/ignoreNetworksare exact string matches (not wildcards), requireevm:<positiveInt>, and are mutually exclusive.ignoreNetworksis dropped fromMarshalJSON.thirdparty/provider.go:L34-49os.ExpandEnvruns on every provider-generated endpoint — a literal$in an API key is substituted from the environment (often to empty). Static upstreams do not get this treatment.thirdparty/provider.go:L63-71- Provider ids must be unique only by convention — no duplicate-id check; two providers with the same default id both run and may produce upstream-id collisions.
thirdparty/providers_registry.go:L15-34 - repository upstream ids embed a redacted endpoint (
https#redacted=ab12c= scheme + 5-char sha256 prefix) so dashboards can distinguish public endpoints without exposing them.util/redact.go:L10-36 - OP-stack
"sender is over rate limit"is the only capacity error withretryableTowardNetwork=false— all OP providers proxy the same sequencer, so retrying on a different upstream hits the same limit. Expect this error to surface to clients on OP-stack chains.architecture/evm/error_normalizer.go:L125-139 - Tenderly
"not found"becomesErrEndpointClientSideException(retryable), notErrEndpointMissingData— the"Unsupported"block in the normalizer intentionally runs after"Not found"to avoid misclassifying Tenderly's bare"not found".architecture/evm/error_normalizer.go:L394-477 - dwellir
OwnsUpstreamonly matchesdwellir://prefix (notevm+dwellir://) or.dwellir.comdomain; theevm+shorthand still works because shorthand conversion runs before vendor lookup.thirdparty/dwellir.go:L173-180 - Live-probe vendors dial out during routing decisions: envio/erpc/pimlico/routemesh/thirdweb perform a real
eth_chainIdHTTP call (10 s timeout) on the provider bootstrap path. Probe clients are cached forever insync.Maps keyed by chain id or"{url}-{chainId}". PrepareUpstreamsForNetworksucceeds as soon as one upstream registers (minReady=1). Slow providers continue adding upstreams in the background without blocking the first request. If all provider tasks finish with zero upstreams, there is a 100 ms grace re-check before the retryableErrNetworkNotSupportedis returned; if tasks are still running, the caller receivesErrNetworkInitializing.upstream/registry.go:L188-293LookupByUpstreamwith an explicit but non-existentvendorNamereturns nil — it does not fall through toOwnsUpstreammatching. Ifupstream.vendorNameis set to an unregistered name, vendor error-mapping is silently lost. Registration order of the 23 vendors determines which vendor wins when multipleOwnsUpstreamchecks return true for the same endpoint.thirdparty/vendors_registry.go:L54-71llamaandroutemeshOwnsUpstreamdo not check their own scheme prefixes:llama.OwnsUpstreamonly matches.llamarpc.com;routemesh.OwnsUpstreamonly matchesvendorName=="routemesh"or endpoint containingroutemes.sh. Both shorthands (llama://,routemesh://) work only because shorthand conversion incommon/defaults.goruns before vendor lookup.thirdparty/llama.go:L95-97thirdparty/routemesh.go:L135-140- dwellir
SupportsNetworkreturns(false, nil)for an unparseable chain id — unlike every other static-map vendor which propagates the parse error.thirdparty/dwellir.go:L109-113
Observability
No provider-specific Prometheus metrics exist. The vendor label on all upstream-level metrics is populated from the attached vendor's Name(), the config's vendorName, or unknown-<rootDomain> for unrecognized endpoints.
| metric | type | key labels | when it fires |
|---|---|---|---|
erpc_upstream_request_total | counter | project, vendor, network, upstream, category, attempt, composite, finality, user, agent_name | Every upstream request attempt |
erpc_upstream_request_duration_seconds | histogram | project, vendor, network, upstream, category, composite, finality, user | Upstream round-trip latency |
erpc_upstream_request_errors_total | counter | project, vendor, network, upstream, category, error, severity, composite, finality, user, agent_name | Failed upstream requests |
Bootstrap task progress is visible in initializer task names: network/<networkId>/provider/<providerId> (scheduling) and network/<networkId>/upstream/<upstreamId> (per-generated upstream). upstream/registry.go:L423-481
Trace spans: buildProviderBootstrapTask emits a detail span per provider task; buildUpstreamBootstrapTask emits one for each registered upstream. upstream/registry.go:L423-485
Notable log messages:
- warn
no providers or upstreams found in project; will use default 'public' endpoints repository - debug
attempting to create upstream(s) from provider/ debugprovider does not support network; skipping upstream creation/ inforegistering N upstream(s) from provider - error
failed to execute provider bootstrap tasks/ infoprovider bootstrap tasks executed successfully - warn
vendor remote-data refresh failed; keeping previous snapshotwithvendor+cacheKeyfields - error
panic recovered during vendor remote-data async refresh - warn
some quicknode chain ID fetches failed; continuing with available data/ warnfailed to fetch chain IDs for some QuickNode endpoints - warn
some chainstack chain ID fetches failed; continuing with available data/ warnfailed to fetch chain IDs for some Chainstack nodes - info
successfully fetched dRPC network names(with count + url) - debug
generated upstream from alchemy provider/generated upstream from conduit provider/generated upstreams from repository provider/generated upstreams from superchain provider
Source code entry points
thirdparty/vendors_registry.go:L9-36— global 23-vendor registry;LookupByName/LookupByUpstream/SupportedVendorsthirdparty/providers_registry.go:L15-34— per-project ProviderConfig → Provider binding; unknown-vendor startup errorthirdparty/provider.go:L13-162—SupportsNetworkgating,GenerateUpstreamConfigs, id templating, env expansionthirdparty/remote_cache.go:L13-78—RemoteDataCache[T]algorithm; request-path safety rationale;ErrRemoteCacheColdcommon/defaults.go:L1113-1241— default-provider injection, shorthand upstream→provider conversioncommon/defaults.go:L1243-1425—buildProviderSettingsper-vendor settings derivationupstream/registry.go:L155-293— lazyPrepareUpstreamsForNetwork, provider task scheduling, readiness wait loopcommon/validation.go:L780-815— ProviderConfig validation rulesarchitecture/evm/error_normalizer.go:L878-900— vendor-specific error hook invocation
Related pages
- Upstreams — static upstream entries; providers generate these dynamically.
- Selection policies — controls which generated upstream handles each request.
- Failsafe — configure retry/hedge/timeout inside
overridesper network. - Rate limiters — cap per-vendor request rates to stay within API budgets.
- Auth — protect your eRPC instance when proxying to paid vendor endpoints.