Use this file to discover all available pages before exploring further.
Every on-chain price update costs gas. Updating too frequently wastes relayer resources and increases costs for the protocol. Updating too infrequently leaves stale data on-chain and exposes consuming protocols to risk. IFÁ Labs resolves this tension with a hybrid trigger model — combining deviation-based and time-based conditions calibrated specifically for stablecoin behavior.Understanding this model is important for anyone building protocols that consume IFÁ Labs feeds — particularly when setting staleness thresholds and designing fallback logic.
When this condition fires, it means the market has moved meaningfully enough to warrant an update — regardless of how recently the last update occurred.
When this condition fires, it means too much time has passed without an update — even if the price hasn’t moved. This guarantees that lastUpdateTime never becomes indefinitely stale during periods of perfect market stability.
Deviation thresholds and heartbeat intervals are configured independently per asset. A single global setting would be either too tight for some assets or too loose for others.
Asset
Deviation Threshold
Heartbeat Interval
Rationale
USDT/USD
0.1%
1 hour
Deep liquidity, tight peg — small moves are meaningful
USDC/USD
0.1%
1 hour
Same profile as USDT
CNGN/USD
0.3%
2 hours
Emerging market asset — slightly wider tolerance for natural micro-fluctuation
ZARP/USD
0.3%
2 hours
Same profile as CNGN
BRZ/USD
0.3%
2 hours
Same profile as CNGN
ETH/USD
0.5%
1 hour
Reference asset — wider deviation threshold appropriate for non-pegged asset
These values represent the current configuration and may be adjusted as the network matures and empirical update data informs calibration. Follow @ifalabs for announcements of threshold changes.
Your protocol’s MAX_PRICE_AGE staleness check needs to account for the heartbeat interval of the assets you’re consuming. If you set MAX_PRICE_AGE tighter than the heartbeat interval, your protocol will frequently reject valid, accurate prices simply because the market hasn’t moved enough to trigger a deviation update.
Before setting staleness thresholds, observe the actual update cadence for your target asset in production. The PriceUpdated event log is the authoritative source:
const { ethers } = require("ethers");const ORACLE_ADDRESS = "0xA9F17344689C2c2328F94464998db1d3e35B80dC";const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");const oracle = new ethers.Contract(ORACLE_ADDRESS, [ "event PriceUpdated(bytes32 indexed assetId, int256 price, int8 decimal, uint64 timestamp)"], provider);const CNGN_ASSET_ID = "0x83a18c73cf75a028a24b79cbedb3b8d8ba363b748a3210ddbcaa95eec3b87b3a";async function getUpdateCadence(assetId, lookbackBlocks = 10000) { const currentBlock = await provider.getBlockNumber(); const events = await oracle.queryFilter( oracle.filters.PriceUpdated(assetId), currentBlock - lookbackBlocks, currentBlock ); if (events.length < 2) { console.log("Insufficient events for cadence analysis"); return; } const intervals = []; for (let i = 1; i < events.length; i++) { const delta = Number(events[i].args.timestamp) - Number(events[i - 1].args.timestamp); intervals.push(delta); } const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length; const max = Math.max(...intervals); const min = Math.min(...intervals); console.log(`Update cadence for asset over last ${lookbackBlocks} blocks:`); console.log(` Average interval: ${Math.round(avg)}s (${(avg/60).toFixed(1)} min)`); console.log(` Longest interval: ${max}s (${(max/60).toFixed(1)} min)`); console.log(` Shortest interval: ${min}s (${(min/60).toFixed(1)} min)`); console.log(` Total updates: ${events.length}`); console.log(`\nRecommended MAX_PRICE_AGE: ${Math.round(max * 1.5)}s`);}getUpdateCadence(CNGN_ASSET_ID);
Run this script against each asset you consume before finalising your staleness threshold configuration. The longest observed interval multiplied by 1.5 is a reliable starting point.
The deviation calculation compares the new aggregated price against the current on-chain stored value — not the previous aggregation round’s result. This distinction matters:
Scenario: On-chain price: 1.000000 Last aggregation result: 1.000080 (no update triggered — below threshold) Current aggregation: 1.001200 (deviation from on-chain = 0.12%) 0.12% > 0.10% threshold → Update submitted New on-chain price: 1.001200
The deviation is always measured against the last submitted on-chain value. This means small moves that individually fall below the threshold can accumulate and eventually trigger an update when the total drift from the last on-chain value exceeds the threshold.
The deviation trigger fires on movement relative to the last on-chain price — not relative to the $1.00 peg. These are different things:
On-chain price: 0.995000 (already 0.5% off peg — submitted earlier)New aggregation: 0.996000 (moved +0.10% from on-chain value)Deviation from on-chain: 0.10% → triggers update if threshold is 0.10%Deviation from peg: 0.40% → would trigger a peg circuit breaker
Protocols implementing peg deviation circuit breakers should check the price against the $1.00 peg directly — not rely on the oracle’s deviation trigger logic for that protection. See Verify Price Integrity for circuit breaker implementation.
Without a heartbeat trigger, a stablecoin that sits perfectly at its peg for days would never update. lastUpdateTime would grow indefinitely stale, and any protocol with a reasonable staleness check would start rejecting the price — even though the price is accurate.The heartbeat prevents this by guaranteeing a maximum gap between updates regardless of price movement.
In practice, heartbeat updates do not fire at perfectly regular intervals. Several factors introduce jitter:
Gas price variability — relayers may delay submission slightly during high gas periods to minimise costs
Block time variability — the exact block in which a transaction lands varies
Aggregation round timing — the heartbeat condition is evaluated per aggregation round, not continuously
Jitter is typically small — seconds to a few minutes — but it is the reason your MAX_PRICE_AGE should include a buffer above the nominal heartbeat interval rather than matching it exactly.
Example: USDT/USD heartbeat = 3,600 secondsObserved submission times: Round N: T + 0s Round N+1: T + 3,587s (13 seconds early — gas was cheap) Round N+2: T + 3,624s (24 seconds late — minor congestion) Round N+3: T + 3,601s (1 second late — normal)Recommendation: Set MAX_PRICE_AGE to 5,400s (90 minutes) for USDT/USD to comfortably accommodate this jitter.