Skip to main content

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:
  1. Pool Data Collection: Gather price, volume, reserve, and depth from all pools
  2. Mode Selection: Choose between volume-based or reserve-based weighting
  3. Outlier Filtering: Remove scam pools and manipulated prices
  4. Weight Calculation: Apply stable multiplier and depth adjustments
  5. 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:
PoolPriceVolumeReservePaired With
Uniswap V3$1.00$5M$10MUSDC
Sushiswap$1.02$2M$4METH
PancakeSwap$0.98$1M$2MUSDT
ScamDEX$0.10$1K$10KETH

Step 2: Mode Selection

The engine chooses a weighting strategy based on available data:
const computeMode: 'volume' | 'reserve' = 
  totalNormalizedVolume > 0 ? 'volume' : 'reserve';
ConditionModeWeight SourceUse Case
Any pool has volume > 0volumenormalizedVolumeActive tokens
All pools have volume = 0reservereserve (TVL)New/inactive tokens
Reserve mode is a fallback for newly listed or low-activity tokens where volume data isn’t reliable.

Step 3: Outlier Filtering (Two-Phase Median)

This is the most critical step. It removes:
  • Scam pools with manipulated prices
  • Stale pools with outdated prices
  • Low-liquidity pools with extreme slippage

Phase 1: Weighted Median Calculation

// 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.50vs0.50 vs 1.00 (50% lower) should be treated the same as 2.00vs2.00 vs 1.00 (100% higher)
  • Log space makes these symmetric: log(0.5) = -0.69, log(2) = 0.69

Filtering Result

After filtering our example:
PoolPriceStatus
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

PoolVolumePairedMultiplierFinal Weight
Uniswap V3$5MUSDC×1$5M
Sushiswap$2METH×3$6M
PancakeSwap$1MUSDT×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

ChainToken PriceVolumeShare
Ethereum$1.001$500M89.3%
BSC$0.999$50M8.9%
Polygon$1.000$10M1.8%
Obscure Chain$0.50$1000.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);
}

Performance Optimizations

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

StepPurposeKey Technique
Data CollectionGather all pool dataMulti-DEX aggregation
Mode SelectionChoose weighting strategyVolume vs Reserve fallback
Outlier FilteringRemove manipulated pricesTwo-phase log-space median
Weight CalculationPrioritize reliable sourcesStable multiplier (×3)
Weighted AverageCompute final priceΣ(price × weight) / Σweight
The result: accurate, manipulation-resistant prices updated in real-time across 100+ blockchains.