Optimized Fee Calculation
Hardcoding relay fees works for prototypes, but production systems need to adapt to market conditions in real time. In this lesson you will calculate the exact fee a relay should receive based on live gas prices, relay-specific pricing, and protective buffers — mirroring the logic in the Nimiq wallet.
Learning Goals
- Fetch the current network gas price and respect the relay’s minimum.
- Apply context-aware buffers that keep transactions reliable without overspending.
- Combine relay percentage fees and base fees into a single POL amount.
- Convert that POL cost into USDT using conservative pricing assumptions.
- Compare multiple relays and pick the most cost-effective option.
The maths mirrors the fee calculation used in the Nimiq wallet. Cross-reference it with the OpenGSN fee model documentation and the Nimiq Developer Center if you want to see the production lineage.
The Core Formula
chainTokenFee = (gasPrice * gasLimit * (1 + pctRelayFee/100)) + baseRelayFeeusdtFee = (chainTokenFee / usdtPriceInPOL) * safetyBufferWhere:
gasPriceis the higher of the network price and the relay’s minimum, optionally buffered.gasLimitdepends on the method you are executing.pctRelayFeeandbaseRelayFeecome from the relay registration event.safetyBufferadds wiggle room (typically 10-50%).usdtPriceInPOLconverts the POL cost into USDT.
Step 1: Read the Network Gas Price
1const networkGasPrice = await provider.getGasPrice()2console.log('Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei')3
4// Get relay's minimum (from the relay discovery lesson)5const relay = await discoverRelay()6const minGasPrice = ethers.BigNumber.from(relay.minGasPrice)7
8// Take the max9const gasPrice = networkGasPrice.gt(minGasPrice) ? networkGasPrice : minGasPriceUsing the maximum of the two ensures you never pay less than the relay requires, while still benefiting from low network prices when possible.
Why the min gas price matters: each relay advertises a floor in its /getaddr response. If you submit a transaction below that price the worker will reject it. The Polygon gas market guide explains why congestion spikes make this floor fluctuate.
Step 2: Apply a Safety Buffer
Different workflows tolerate risk differently. Adjust the buffer based on the method or environment you are in.
1const ENV_MAIN = true // Set based on environment2
3let bufferPercentage4if (method === 'redeemWithSecretInData') {5 bufferPercentage = 150 // 50% buffer (swap fee volatility)6}7else if (ENV_MAIN) {8 bufferPercentage = 120 // 20% buffer (mainnet)9}10else {11 bufferPercentage = 125 // 25% buffer (testnet, more volatile)12}13
14const bufferedGasPrice = gasPrice.mul(bufferPercentage).div(100)15console.log('Buffered gas price:', ethers.utils.formatUnits(bufferedGasPrice, 'gwei'), 'gwei')Step 3: Get Gas Limit from Transfer Contract
Instead of hardcoding gas limits, query the transfer contract directly:
1const TRANSFER_CONTRACT_ABI = [2 'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)'3]4
5const transferContract = new ethers.Contract(6 TRANSFER_CONTRACT_ADDRESS,7 TRANSFER_CONTRACT_ABI,8 provider9)10
11// Method selector for transferWithApproval12const METHOD_SELECTOR = '0x8d89149b'13const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR)14
15console.log('Gas limit:', gasLimit.toString())This ensures your gas estimates stay accurate even if the contract changes.
Step 4: Combine Gas Costs and Relay Fees
1// Calculate base cost2const baseCost = bufferedGasPrice.mul(gasLimit)3
4// Add relay percentage fee5const pctRelayFee = 15 // From relay registration (e.g., 15%)6const costWithPct = baseCost.mul(100 + pctRelayFee).div(100)7
8// Add base relay fee9const baseRelayFee = ethers.BigNumber.from(relay.baseRelayFee)10const totalChainTokenFee = costWithPct.add(baseRelayFee)11
12console.log('Total POL cost:', ethers.utils.formatEther(totalChainTokenFee))This yields the amount of POL the relay expects to receive after covering gas.
Step 5: Get Real-Time POL/USDT Price from Uniswap
Query Uniswap V3 for the current exchange rate:
1const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'2const WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'3const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7'4
5const POOL_ABI = ['function fee() external view returns (uint24)']6const QUOTER_ABI = [7 'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)'8]9
10async function getPolUsdtPrice(provider) {11 const pool = new ethers.Contract(USDT_WMATIC_POOL, POOL_ABI, provider)12 const quoter = new ethers.Contract(UNISWAP_QUOTER, QUOTER_ABI, provider)13
14 const fee = await pool.fee()15
16 // Quote: How much POL for 1 USDT?17 const polPerUsdt = await quoter.callStatic.quoteExactInputSingle(18 USDT_ADDRESS,19 WMATIC,20 fee,21 ethers.utils.parseUnits('1', 6),22 023 )24
25 return polPerUsdt // POL wei per 1 USDT26}Step 6: Convert POL Cost to USDT
1const polPerUsdt = await getPolUsdtPrice(provider)2
3// Convert: totalPOLCost / polPerUsdt = USDT units4// No additional buffer - gas price buffer is sufficient5const feeInUSDT = totalChainTokenFee6 .mul(1_000_000)7 .div(polPerUsdt)8
9console.log('USDT fee:', ethers.utils.formatUnits(feeInUSDT, 6))Using Uniswap ensures your fees reflect current market rates, preventing underpayment when POL appreciates.
Step 7: Reject Expensive Relays
1const MAX_PCT_RELAY_FEE = 70 // Never accept >70%2const MAX_BASE_RELAY_FEE = 0 // Never accept base fee3
4if (pctRelayFee > MAX_PCT_RELAY_FEE) {5 throw new Error(`Relay fee too high: ${pctRelayFee}%`)6}7
8if (baseRelayFee.gt(MAX_BASE_RELAY_FEE)) {9 throw new Error('Relay base fee not acceptable')10}These guardrails prevent accidental overpayment when a relay is misconfigured or opportunistic.
Step 8: Choose the Best Relay
1async function getBestRelay(relays, gasLimit, method) {2 let bestRelay = null3 let lowestFee = ethers.constants.MaxUint2564
5 for (const relay of relays) {6 try {7 const fee = await calculateFee(relay, gasLimit, method)8 if (fee.lt(lowestFee)) {9 lowestFee = fee10 bestRelay = relay11 }12 }13 catch (error) {14 continue // Skip invalid relays15 }16 }17
18 return bestRelay19}When you have multiple candidates, this loop compares their fees and picks the cheapest valid option.
Want more than “cheapest wins”? It is common to score relays on latency, historical success rate, or geographic proximity. The OpenGSN Relay Operator checklist lists the metrics most teams monitor.
Production Considerations
These extras come straight from the Nimiq wallet codebase:
- Timeout relay requests after two seconds to avoid hanging the UI.
- Cap the number of relays you test (for example, at 10) to keep discovery fast.
- Require worker balances at least twice the expected gas cost for safety.
- Skip inactive relays unless they belong to trusted providers such as Fastspot.
- Use generators or iterators so you can stop searching the moment a good relay appears.
Putting It All Together
1async function calculateOptimalFee(relay, provider, transferContract) {2 // 1. Get gas prices3 const networkGasPrice = await provider.getGasPrice()4 const baseGasPrice = networkGasPrice.gt(relay.minGasPrice)5 ? networkGasPrice6 : relay.minGasPrice7
8 // 2. Apply buffer9 const bufferedGasPrice = baseGasPrice.mul(120).div(100) // 20% mainnet10
11 // 3. Get gas limit from contract12 const gasLimit = await transferContract.getRequiredRelayGas('0x8d89149b')13
14 // 4. Calculate POL fee15 const baseCost = bufferedGasPrice.mul(gasLimit)16 const withPctFee = baseCost.mul(100 + relay.pctRelayFee).div(100)17 const totalPOL = withPctFee.add(relay.baseRelayFee)18
19 // 5. Convert to USDT via Uniswap20 const polPerUsdt = await getPolUsdtPrice(provider)21 const usdtFee = totalPOL.mul(1_000_000).div(polPerUsdt)22
23 return { usdtFee, gasPrice: bufferedGasPrice, gasLimit }24}Reuse this helper whenever you prepare a meta-transaction so each request reflects current network conditions.
Wrap-Up
You now have a production-grade fee engine that:
- ✅ Tracks live gas prices and relay minimums.
- ✅ Queries contract for accurate gas limits.
- ✅ Uses Uniswap V3 for real-time POL/USDT rates.
- ✅ Applies thoughtful buffers to avoid underpayment.
- ✅ Compares relays and selects the most cost-effective option.
At this point your gasless transaction pipeline matches the approach we ship in the Nimiq wallet - ready for real users. The next lesson covers USDC transfers using the EIP-2612 permit approval method, showing how different tokens require different approval strategies.
- npm install
- npm run optimized