Pricing Engine: Weighted Averages & Outlier Filtering
Mobula’s pricing engine aggregates price data from hundreds of liquidity pools across multiple chains to compute a single, reliable USD price for every token. This guide explains the mathematics and algorithms behind accurate price discovery.
Why it matters: A token like WETH might trade at slightly different prices on Uniswap, Sushiswap, Curve, and 50 other DEXs. Our engine combines all these prices into one reliable value, filtering out manipulated pools and scams.
Overview: The Pricing Pipeline
Pool Data Collection → Mode Selection → Outlier Filtering → Weight Calculation → Weighted Average
Each step is critical for accuracy:
- Pool Data Collection: Gather price, volume, reserve, and depth from all pools
- Mode Selection: Choose between volume-based or reserve-based weighting
- Outlier Filtering: Remove scam pools and manipulated prices
- Weight Calculation: Apply stable multiplier and depth adjustments
- Weighted Average: Compute final price
Step 1: Pool Data Collection
For each pool trading a token, we extract:
interface PoolWeight {
price: number; // Token's USD price in this pool
normalizedVolume: number; // 24h volume (0 if below threshold)
realVolume: number; // Actual 24h volume for metrics
reserve: number; // Total liquidity (TVL)
depthUp: number; // Buy-side market depth
depthDown: number; // Sell-side market depth
}
Example: Token XYZ trading on 4 pools:
| Pool | Price | Volume | Reserve | Paired With |
|---|
| Uniswap V3 | $1.00 | $5M | $10M | USDC |
| Sushiswap | $1.02 | $2M | $4M | ETH |
| PancakeSwap | $0.98 | $1M | $2M | USDT |
| ScamDEX | $0.10 | $1K | $10K | ETH |
Step 2: Mode Selection
The engine chooses a weighting strategy based on available data:
const computeMode: 'volume' | 'reserve' =
totalNormalizedVolume > 0 ? 'volume' : 'reserve';
| Condition | Mode | Weight Source | Use Case |
|---|
| Any pool has volume > 0 | volume | normalizedVolume | Active tokens |
| All pools have volume = 0 | reserve | reserve (TVL) | New/inactive tokens |
Reserve mode is a fallback for newly listed or low-activity tokens where volume data isn’t reliable.
This is the most critical step. It removes:
- Scam pools with manipulated prices
- Stale pools with outdated prices
- Low-liquidity pools with extreme slippage
// Sort prices by value
const sortedPrices = prices.sort((a, b) => a - b);
// Find weighted median
let accumulatedWeight = 0;
let medianPrice = 0;
for (let i = 0; i < sortedPrices.length; i++) {
accumulatedWeight += weights[i];
if (accumulatedWeight >= totalWeight / 2) {
medianPrice = sortedPrices[i];
break;
}
}
Phase 2: Log-Space Deviation Check
// Calculate deviation in log space (handles multiplicative differences)
const logPrice = Math.log(price);
const logMedian = Math.log(medianPrice);
const deviation = Math.abs(logPrice - logMedian);
// Threshold: 10% in volume mode, 15% in reserve mode (more permissive)
const threshold = computeMode === 'reserve'
? BASE_THRESHOLD * 1.5
: BASE_THRESHOLD;
const isOutlier = deviation > threshold;
Why log space?
- A price of 0.50vs1.00 (50% lower) should be treated the same as 2.00vs1.00 (100% higher)
- Log space makes these symmetric:
log(0.5) = -0.69, log(2) = 0.69
Filtering Result
After filtering our example:
| Pool | Price | Status |
|---|
| Uniswap V3 | $1.00 | ✅ Valid (near median) |
| Sushiswap | $1.02 | ✅ Valid (2% deviation) |
| PancakeSwap | $0.98 | ✅ Valid (2% deviation) |
| ScamDEX | $0.10 | ❌ Outlier (90% deviation) |
Step 4: Weight Calculation
Stable Multiplier
Pools paired with stablecoins provide more reliable pricing than pools paired with volatile assets:
const STABLE_MULTIPLIER = 3;
let weight = normalizedVolume;
// Non-stable pairs get boosted weight to compensate for their
// typically lower volume but higher price accuracy
if (!baseQuote.isStableCoin({ chainId, address })) {
weight *= STABLE_MULTIPLIER;
}
Why boost non-stable pairs?
- Stable-paired pools (USDC, USDT) give direct USD prices
- ETH-paired pools require ETH→USD conversion, introducing small errors
- But ETH pairs often have MORE volume, so we boost stable pairs to balance
Depth Weighting (Optional)
When ponderWithDepth = true:
if (ponderWithDepth) {
const depth = depthUp + depthDown;
weight = depth > 0 ? normalizedVolume * depth : normalizedVolume;
}
This prioritizes pools where large trades can execute with minimal slippage.
Final Weights Example
| Pool | Volume | Paired | Multiplier | Final Weight |
|---|
| Uniswap V3 | $5M | USDC | ×1 | $5M |
| Sushiswap | $2M | ETH | ×3 | $6M |
| PancakeSwap | $1M | USDT | ×1 | $1M |
Step 5: Weighted Average
The final price is computed as:
function computeWeightedMean(
prices: number[],
weights: number[],
count: number
): [number, number] {
let weightedSum = 0;
let totalWeight = 0;
for (let i = 0; i < count; i++) {
weightedSum += prices[i] * weights[i];
totalWeight += weights[i];
}
const price = weightedSum / totalWeight;
return [price, totalWeight];
}
Example Calculation
numerator = (1.00 × 5M) + (1.02 × 6M) + (0.98 × 1M)
= 5M + 6.12M + 0.98M
= 12.1M
denominator = 5M + 6M + 1M = 12M
finalPrice = 12.1M / 12M = $1.0083
Asset-Level Pricing
For tokens deployed on multiple chains, we aggregate token prices into a single asset price.
Why Aggregate?
The same token can have different prices on different chains due to:
- Bridge delays and costs
- Chain-specific liquidity
- Arbitrage opportunities
Aggregation Algorithm
computeAssetPrice(tokenResults: Map<SelectorStr, TokenResult>) {
const count = tokenResults.size;
if (count === 0) return null;
// Determine weighting mode
let totalVolume = 0;
let totalReserve = 0;
for (const data of tokenResults.values()) {
totalVolume += data.volume;
totalReserve += data.reserve;
}
const useVolume = totalVolume > 0;
const total = useVolume ? totalVolume : totalReserve;
// Filter by threshold (1% minimum share)
const VOLUME_THRESHOLD = 0.01;
const validTokens = [];
for (const data of tokenResults.values()) {
const share = useVolume
? data.volume / total
: data.reserve / total;
if (share >= VOLUME_THRESHOLD) {
validTokens.push(data);
}
}
// Weighted average
return computeWeightedMean(
validTokens.map(t => t.price),
validTokens.map(t => useVolume ? t.volume : t.reserve),
validTokens.length
);
}
Example: USDT Asset Price
| Chain | Token Price | Volume | Share |
|---|
| Ethereum | $1.001 | $500M | 89.3% |
| BSC | $0.999 | $50M | 8.9% |
| Polygon | $1.000 | $10M | 1.8% |
| Obscure Chain | $0.50 | $100 | 0.00002% ← Filtered |
assetPrice = (1.001×500M + 0.999×50M + 1.000×10M) / 560M
= $1.0008
Outlier Detection for Assets
When aggregating across chains, we also filter token-level outliers:
const ASSET_TOKEN_OUTLIER_THRESHOLD = 2.0; // 200% deviation
for (const token of filteredTokens) {
const priceDeviation = Math.abs(token.price - assetPrice) / assetPrice;
if (priceDeviation > ASSET_TOKEN_OUTLIER_THRESHOLD) {
outlierTokens.push(token);
continue;
}
validTokens.push(token);
}
// Re-compute price without outliers
if (outlierTokens.length > 0 && validTokens.length > 0) {
return computeWeightedMean(validTokens);
}
Pre-allocated Buffers
// Avoid allocations in hot path
private readonly priceBuffer: Float64Array;
private readonly weightBuffer: Float64Array;
private readonly logPriceBuffer: Float64Array;
constructor(maxPoolsPerToken = 256) {
this.priceBuffer = new Float64Array(maxPoolsPerToken);
this.weightBuffer = new Float64Array(maxPoolsPerToken);
this.logPriceBuffer = new Float64Array(maxPoolsPerToken);
}
Selector Caching
private readonly selectorCache: Map<SelectorStr, { chainId: ChainId; address: string }>;
private parseSelectorCached(selector: SelectorStr) {
let cached = this.selectorCache.get(selector);
if (!cached) {
cached = parseSelector(selector);
if (this.selectorCache.size < 1000) {
this.selectorCache.set(selector, cached);
}
}
return cached;
}
Validation & Safety
Price Bounds
const INSANE_PRICE_THRESHOLD = 1e15; // $1 quadrillion
isValidPrice(price: number): boolean {
return price > 0 &&
price < INSANE_PRICE_THRESHOLD &&
Number.isFinite(price);
}
Minimum Valid Prices
const MIN_VALID_PRICES = 2;
// Require at least 2 valid pools for reliable pricing
if (validIndices.length < MIN_VALID_PRICES) {
return null;
}
API Access
Token Price
# Get price for Fartcoin on Solana
curl -X GET "https://demo-api.mobula.io/api/2/token/details?blockchain=solana&address=9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump" \
-H "Authorization: YOUR_API_KEY"
# Get price for Wojak on Solana
curl -X GET "https://demo-api.mobula.io/api/2/token/details?blockchain=solana&address=8J69rbLTzWWgUJziFY8jeu5tDwEPBwUz4pKBMr5rpump" \
-H "Authorization: YOUR_API_KEY"
# Get WETH price on Ethereum
curl -X GET "https://demo-api.mobula.io/api/2/token/details?blockchain=ethereum&address=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" \
-H "Authorization: YOUR_API_KEY"
Response includes:
{
"data": {
"priceUSD": 0.0234,
"volume24hUSD": 890000.50,
"liquidityUSD": 250000.75,
"priceChange24hPercentage": -5.8,
"approximateReserveUSD": 125000.50
}
}
Real-Time Updates
For real-time price updates, use the WSS Token Details WebSocket stream.
Summary
| Step | Purpose | Key Technique |
|---|
| Data Collection | Gather all pool data | Multi-DEX aggregation |
| Mode Selection | Choose weighting strategy | Volume vs Reserve fallback |
| Outlier Filtering | Remove manipulated prices | Two-phase log-space median |
| Weight Calculation | Prioritize reliable sources | Stable multiplier (×3) |
| Weighted Average | Compute final price | Σ(price × weight) / Σweight |
The result: accurate, manipulation-resistant prices updated in real-time across 100+ blockchains.