Discovering Relays Dynamically

Hardcoding a relay URL works for demos, but production code needs to discover healthy relays automatically. In this lesson, you will query the OpenGSN RelayHub contract, vet the results, and pick a relay that is ready to carry your transaction. This approach mirrors what the Nimiq wallet uses in production.


Objectives

By the end of this lesson you will:

  • Query on-chain events with ethers.js.
  • Check each relay’s advertised metadata and on-chain balances.
  • Filter out relays that are offline, outdated, or underfunded.
  • Produce a resilient fallback chain when the preferred relay fails.

Before you start, skim the reference material so the field names feel familiar:


Step 1: Pull Recent Relay Registrations

RelayHub emits a RelayServerRegistered event whenever a relay announces itself. Scan the recent blocks to collect candidates.

discover-relays.ts
1
const RELAY_HUB_ABI = ['event RelayServerRegistered(address indexed relayManager, uint256 baseRelayFee, uint256 pctRelayFee, string relayUrl)']
2
const relayHub = new ethers.Contract(RELAY_HUB_ADDRESS, RELAY_HUB_ABI, provider)
3
4
const currentBlock = await provider.getBlockNumber()
5
const LOOKBACK_BLOCKS = 14400 // ~10 hours on Polygon
6
7
const events = await relayHub.queryFilter(
8
relayHub.filters.RelayServerRegistered(),
9
currentBlock - LOOKBACK_BLOCKS,
10
currentBlock
11
)
12
13
const seen = new Map()
14
15
for (const event of events) {
16
const { relayManager, baseRelayFee, pctRelayFee, relayUrl } = event.args
17
if (!seen.has(relayUrl)) {
18
seen.set(relayUrl, {
19
url: relayUrl,
20
relayManager,
21
baseRelayFee,
22
pctRelayFee,
23
})
24
}
25
}
26
27
const candidates = Array.from(seen.values())
28
console.log(`Found ${candidates.length} unique relay URLs`)

Looking back roughly 10 hours balances freshness with performance. Adjust the window if you need more or fewer candidates.


Step 2: Ping and Validate Each Relay

For every registration, call the /getaddr endpoint and run a series of health checks before trusting it.

validate-relay.ts
1
async function validateRelay(relay, provider) {
2
try {
3
const controller = new AbortController()
4
const timeout = setTimeout(() => controller.abort(), 10_000)
5
6
const response = await fetch(`${relay.url}/getaddr`, { signal: controller.signal })
7
8
clearTimeout(timeout)
9
10
if (!response.ok)
11
return null
12
13
const relayInfo = await response.json()
14
15
if (!relayInfo.version?.startsWith('2.'))
16
return null
17
if (relayInfo.networkId !== '137' && relayInfo.chainId !== '137')
18
return null
19
if (!relayInfo.ready)
20
return null
21
22
const workerBalance = await provider.getBalance(relayInfo.relayWorkerAddress)
23
if (workerBalance.lt(ethers.utils.parseEther('0.01')))
24
return null
25
26
const pctFee = Number.parseInt(relay.pctRelayFee)
27
const baseFee = ethers.BigNumber.from(relay.baseRelayFee)
28
29
if (pctFee > 70 || baseFee.gt(0))
30
return null
31
32
return {
33
...relay,
34
relayWorkerAddress: relayInfo.relayWorkerAddress,
35
minGasPrice: relayInfo.minGasPrice,
36
version: relayInfo.version,
37
}
38
}
39
catch (error) {
40
return null // Relay offline or invalid
41
}
42
}

AbortController gives us a portable timeout without extra dependencies, which keeps the sample compatible with both Node.js scripts and browser bundlers.

Checks to keep in mind:

  • Version must start with 2.x to match the OpenGSN v2 protocol.
  • Network / chain ID should be 137 for Polygon mainnet.
  • Worker balance needs enough POL to front your transaction (the example uses 0.01 POL as a floor).
  • Readiness flag confirms the relay advertises itself as accepting requests.
  • Fee caps ensure you never accept a base fee or a percentage beyond your policy.

Step 3: Select the First Healthy Relay

Iterate through the registrations until you find one that passes validation. You can collect alternates for fallback if desired.

find-best-relay.ts
1
async function findBestRelay(provider) {
2
console.log('\n🔬 Validating relays...')
3
4
for (const relay of candidates) {
5
const validRelay = await validateRelay(relay, provider)
6
if (validRelay)
7
return validRelay
8
}
9
10
throw new Error('No valid relays found')
11
}
12
13
const relay = await findBestRelay(provider)
14
console.log('✅ Using relay:', relay.url)

This simple loop already improves reliability dramatically compared to a hardcoded URL.


Why This Beats a Static Relay

  • ✅ Automatically skips relays that are offline or misconfigured.
  • ✅ Picks up newly registered relays without code changes.
  • ✅ Gives you hooks to rank relays by price, latency, or trust level.
  • ❌ Still relies on static fee estimates (we will tackle that in the next lesson).

Wrap-Up

You now have a discovery pipeline that:

  • ✅ Queries RelayHub for fresh relay registrations.
  • ✅ Validates each relay’s network, version, balance, and responsiveness.
  • ✅ Falls back gracefully when a relay fails health checks.
  • ✅ Removes the last hardcoded relay URL from your workflow.

Next up: Optimized Fee Calculation, where you replace static fees with a dynamic, production-ready calculation.

Powered by WebContainers
Files
Preparing Environment
  • npm install
  • npm run discover