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

Fee formula
chainTokenFee = (gasPrice * gasLimit * (1 + pctRelayFee/100)) + baseRelayFee
usdtFee = (chainTokenFee / usdtPriceInPOL) * safetyBuffer

Where:

  • gasPrice is the higher of the network price and the relay’s minimum, optionally buffered.
  • gasLimit depends on the method you are executing.
  • pctRelayFee and baseRelayFee come from the relay registration event.
  • safetyBuffer adds wiggle room (typically 10-50%).
  • usdtPriceInPOL converts the POL cost into USDT.

Step 1: Read the Network Gas Price

gas-price.ts
1
const networkGasPrice = await provider.getGasPrice()
2
console.log('Network gas price:', ethers.utils.formatUnits(networkGasPrice, 'gwei'), 'gwei')
3
4
// Get relay's minimum (from the relay discovery lesson)
5
const relay = await discoverRelay()
6
const minGasPrice = ethers.BigNumber.from(relay.minGasPrice)
7
8
// Take the max
9
const gasPrice = networkGasPrice.gt(minGasPrice) ? networkGasPrice : minGasPrice

Using 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.

buffer.ts
1
const ENV_MAIN = true // Set based on environment
2
3
let bufferPercentage
4
if (method === 'redeemWithSecretInData') {
5
bufferPercentage = 150 // 50% buffer (swap fee volatility)
6
}
7
else if (ENV_MAIN) {
8
bufferPercentage = 120 // 20% buffer (mainnet)
9
}
10
else {
11
bufferPercentage = 125 // 25% buffer (testnet, more volatile)
12
}
13
14
const bufferedGasPrice = gasPrice.mul(bufferPercentage).div(100)
15
console.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:

gas-limit.ts
1
const TRANSFER_CONTRACT_ABI = [
2
'function getRequiredRelayGas(bytes4 methodId) view returns (uint256)'
3
]
4
5
const transferContract = new ethers.Contract(
6
TRANSFER_CONTRACT_ADDRESS,
7
TRANSFER_CONTRACT_ABI,
8
provider
9
)
10
11
// Method selector for transferWithApproval
12
const METHOD_SELECTOR = '0x8d89149b'
13
const gasLimit = await transferContract.getRequiredRelayGas(METHOD_SELECTOR)
14
15
console.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

fee-components.ts
1
// Calculate base cost
2
const baseCost = bufferedGasPrice.mul(gasLimit)
3
4
// Add relay percentage fee
5
const pctRelayFee = 15 // From relay registration (e.g., 15%)
6
const costWithPct = baseCost.mul(100 + pctRelayFee).div(100)
7
8
// Add base relay fee
9
const baseRelayFee = ethers.BigNumber.from(relay.baseRelayFee)
10
const totalChainTokenFee = costWithPct.add(baseRelayFee)
11
12
console.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:

uniswap-price.ts
1
const UNISWAP_QUOTER = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
2
const WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
3
const USDT_WMATIC_POOL = '0x9B08288C3Be4F62bbf8d1C20Ac9C5e6f9467d8B7'
4
5
const POOL_ABI = ['function fee() external view returns (uint24)']
6
const QUOTER_ABI = [
7
'function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) external returns (uint256 amountOut)'
8
]
9
10
async 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
0
23
)
24
25
return polPerUsdt // POL wei per 1 USDT
26
}

Step 6: Convert POL Cost to USDT

fee-to-usdt.ts
1
const polPerUsdt = await getPolUsdtPrice(provider)
2
3
// Convert: totalPOLCost / polPerUsdt = USDT units
4
// No additional buffer - gas price buffer is sufficient
5
const feeInUSDT = totalChainTokenFee
6
.mul(1_000_000)
7
.div(polPerUsdt)
8
9
console.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

guardrails.ts
1
const MAX_PCT_RELAY_FEE = 70 // Never accept >70%
2
const MAX_BASE_RELAY_FEE = 0 // Never accept base fee
3
4
if (pctRelayFee > MAX_PCT_RELAY_FEE) {
5
throw new Error(`Relay fee too high: ${pctRelayFee}%`)
6
}
7
8
if (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

choose-relay.ts
1
async function getBestRelay(relays, gasLimit, method) {
2
let bestRelay = null
3
let lowestFee = ethers.constants.MaxUint256
4
5
for (const relay of relays) {
6
try {
7
const fee = await calculateFee(relay, gasLimit, method)
8
if (fee.lt(lowestFee)) {
9
lowestFee = fee
10
bestRelay = relay
11
}
12
}
13
catch (error) {
14
continue // Skip invalid relays
15
}
16
}
17
18
return bestRelay
19
}

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:

  1. Timeout relay requests after two seconds to avoid hanging the UI.
  2. Cap the number of relays you test (for example, at 10) to keep discovery fast.
  3. Require worker balances at least twice the expected gas cost for safety.
  4. Skip inactive relays unless they belong to trusted providers such as Fastspot.
  5. Use generators or iterators so you can stop searching the moment a good relay appears.

Putting It All Together

calculate-optimal-fee.ts
1
async function calculateOptimalFee(relay, provider, transferContract) {
2
// 1. Get gas prices
3
const networkGasPrice = await provider.getGasPrice()
4
const baseGasPrice = networkGasPrice.gt(relay.minGasPrice)
5
? networkGasPrice
6
: relay.minGasPrice
7
8
// 2. Apply buffer
9
const bufferedGasPrice = baseGasPrice.mul(120).div(100) // 20% mainnet
10
11
// 3. Get gas limit from contract
12
const gasLimit = await transferContract.getRequiredRelayGas('0x8d89149b')
13
14
// 4. Calculate POL fee
15
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 Uniswap
20
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.

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