networks
One networks: list feeds both embedded engines: indexer settings and relayer
settings live side by side on the same entry. Optional: pure off-chain projects
(cron → HTTP → notify) declare no networks and neither engine boots.
rflow_version: 1
name: transfer-alert
config:
port: 3940
db_connection: ${DATABASE_URL}
networks:
- name: ethereum
chain_id: 1
rpc: # structured form: roles + failover policy
primary: ${ETH_RPC_ALCHEMY}
fallbacks:
- ${ETH_RPC_INFURA}
policy:
failover_after: 3 # consecutive errors before failover
max_lag: 3 # blocks behind the best head before eviction
ws: ${ETH_WS} # RESERVED - parsed but not wired yet (validate warns)
confirmations: 12 # relayer CONFIRMED depth (default 12)
- name: base
chain_id: 8453
rpc: ${BASE_RPC} # the simple single-url form still works
contracts:
USDC:
abi: ./abis/erc20.json
addresses:
ethereum: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
notifications:
channels:
ops:
console: {}
workflows:
transfer-alert:
trigger:
event:
contract: USDC
name: Transfer
network: ethereum
steps:
- id: alert
notify:
channel: ops
message: "transfer: ${{ trigger.tx_hash }}"Core fields
| Field | Required | Description |
|---|---|---|
name | ✅ | Your label for the chain, referenced by contracts, triggers and steps |
chain_id | ✅ | The EVM chain id |
rpc | ✅ | Provider endpoint(s): a single url, a list (first = primary, rest = fallbacks), or the structured { primary, fallbacks, policy } form. See failover for per-consumer semantics |
ws | Reserved, not wired yet. Neither engine consumes it (head tracking is HTTP polling; tune block_poll_frequency instead). rflow validate warns when set | |
confirmations | Depth at which the relayer marks a tx CONFIRMED (default 12). Not to be confused with per-trigger confirmations | |
signing_provider | Per-network signing provider override |
Multiple providers & failover
rpc: accepts three equivalent shapes. All normalize to an ordered endpoint
list (primary first) plus a failover policy:
rpc: ${ETH_RPC} # one url
rpc: [${ETH_RPC}, ${ETH_RPC_FALLBACK}] # list: first = primary, rest = fallbacks (default policy)
rpc: # structured: explicit roles + policy
primary: ${ETH_RPC_ALCHEMY}
fallbacks:
- ${ETH_RPC_INFURA}
- ${ETH_RPC_QUICKNODE}
policy:
failover_after: 3
max_lag: 3policy fields
| Field | Default | Description |
|---|---|---|
failover_after | 3 | Consecutive errors (in-band calls or the health probe) before an endpoint is marked unhealthy and traffic moves to the first healthy fallback. Re-admission takes two consecutive within-lag probes (a recovered primary reclaims the traffic), then probation under real traffic; the requirement doubles per flap (capped), so a flapping endpoint cannot thrash the active slot |
max_lag | 3 | Blocks an endpoint may trail the best-known head (probed per endpoint every ~10s) before eviction. Catches a lagging-but-alive provider that error counting never would. "Best-known" remembers past rounds (a stale endpoint cannot measure lag against its own frozen head), and only the probe loop clears a lag eviction. Dev-node escape hatch: when every endpoint agrees the head went backwards (a restarted anvil), the remembered best resets |
These are the only policy keys. The once-reserved read_quorum /
simulation_quorum are rejected as unknown; remove them from configs that
carried them.
Chain ids are verified per endpoint: an endpoint that answers eth_chainId
with anything other than the declared chain_id is permanently evicted and
never serves a request. rflow start verifies every endpoint eagerly and
refuses to boot on a mismatch (a merely unreachable endpoint only warns and
counts a failure), and
rflow validate --preflight
fails naming the exact endpoint before you ever boot. The guarantee does not
depend on the eager pass: the transport gate re-runs eth_chainId on an
endpoint's first admitted request, so an endpoint that was down at boot
and comes back on the wrong chain is evicted before answering a single read,
on every path (replay/test, MCP, CLI one-shots). When every endpoint is
unhealthy the stack fails open to the full set rather than refusing to serve
(wrong-chain endpoints stay excluded). Every request carries a 15s
transport timeout, so a black-holed provider surfaces as a counted failure
instead of a hang.
What fails over — and how, per consumer
All three consumers honor the full url list; their failover semantics differ:
- rflow's own RPC path (reads/simulation).
read:steps, read/block trigger polls,send_transactionpre-flight simulation and gas estimation,simulate:steps,wait_forconfirmation/finality polls, receipt checks, replay/test, MCP reads and the stall monitor all ride the health-aware stack. Strict primary/standby: the first healthy endpoint in list order serves;failover_after/max_lagevict it; a recovered primary reclaims the slot. - The indexer. The embedded rindexer receives the full list and serves
from one sticky active endpoint, rotating on transport failures, a
stalled or lagging tip (its own ~10s prober), or a wrong chain id. It
suspends reorg parent-hash validation across a switch so a laggier
fallback cannot trigger a spurious rollback. Every switch is surfaced
(
/metricsrflow_rpc_endpoint_events_total, thelast failovercolumn inrflow status,/health). - Broadcasts. The embedded rrelayer tracks per-endpoint health (call outcomes + a ~10s tip prober) and selects per call among healthy, chain-verified, non-lagging endpoints: health-aware weighted selection, not a sticky primary (a cooling-down or lagging endpoint is skipped; when everything looks unhealthy the full set serves rather than nothing). Gas estimation follows the same pool (not pinned to url[0]), a wrong-chain url is rejected at boot, and receipt polls for a freshly broadcast tx stick to the endpoint that accepted it until first sighting, so a lagging endpoint cannot trigger premature same-nonce gas bumps.
policy numbers (failover_after, max_lag) tune rflow's own stack;
the engines apply their own equivalent thresholds internally. rflow status
and /health show all three views (consumers reads / indexer /
relayer). See observability.
rflow validate also warns on duplicated urls (a duplicate fails exactly
when its twin does) and on non-http(s) schemes.
rindexer passthroughs
All optional; passed verbatim to the embedded indexer (semantics documented at rindexer.xyz):
| Field | What it controls |
|---|---|
block_poll_frequency | How often the indexer polls for new blocks |
max_block_range | Max block span per eth_getLogs request |
compute_units_per_second | Rate limiting for CU-metered providers |
get_logs_settings | Advanced log-fetch tuning |
multicall3_address | Override the Multicall3 address on exotic chains |
reorg_handling | rindexer reorg detection tuning |
rrelayer passthroughs
All optional; passed verbatim to the embedded relayer (semantics documented at rrelayer.xyz):
| Field | What it controls |
|---|---|
gas_provider | Gas price oracle (fallback / blocknative / infura / tenderly / etherscan / custom) |
gas_bump_blocks_every | How many blocks before a pending tx's gas is bumped |
max_gas_price_multiplier | Cap on gas escalation |
enable_sending_blobs | Allow EIP-4844 blob transactions |
permissions | Allowlists, disable_native_transfer, disable_transactions, ... |
api_keys | Network-scoped API keys for the (opt-in) relayer API |
automatic_top_up | Relayers fund each other / from a Safe when balances run low |
Example top-up where the treasury relayer keeps every other relayer above 0.1 native:
rflow_version: 1
name: treasury-top-up
config:
port: 3940
db_connection: ${DATABASE_URL}
# DEV ONLY raw mnemonic - swap in a production signer before real funds
signer:
raw:
mnemonic: ${RAW_DANGEROUS_MNEMONIC}
relayers:
treasury:
networks: [ethereum]
payout:
networks: [ethereum]
networks:
- name: ethereum
chain_id: 1
rpc: ${ETH_RPC}
automatic_top_up:
- from:
relayer:
address: "${{ relayers.treasury.address }}"
internal_only: true
relayers: "*"
native: { min_balance: "0.1", top_up_amount: "0.2" }
workflows:
payouts:
trigger:
webhook: { path: /hooks/payout }
steps:
- id: send
send_transaction:
network: ethereum
relayer: payout
to: "${{ trigger.args.to }}"
value: "${{ trigger.args.amount }}"rflow validates these blocks when it generates the runtime engine configs at boot
(.rflow/runtime/{rindexer,rrelayer}/). Env placeholders are preserved, so secrets
never land on disk.