Skip to main content

Getting Price Data in Your Indexer

Introduction

Many blockchain applications require price data to calculate values such as:

  • Historical token transfer values in USD
  • Total value locked (TVL) in DeFi protocols over time
  • Portfolio valuations at specific points in time

This tutorial explores three different approaches to incorporating price data into your Envio indexer, using a real-world example of tracking ETH deposits into a Uniswap V3 liquidity pool on the Blast blockchain.

TL;DR: The complete code for this tutorial is available in this GitHub repository.

What You'll Learn

In this tutorial, you'll:

  • Compare three different methods for accessing token price data
  • Analyze the tradeoffs between accuracy, decentralization, and performance
  • Implement a multi-source price feed in an Envio indexer
  • Build a practical example indexing Uniswap V3 liquidity events with price context

Price Data Methods Compared

There are three primary methods to access price data within your indexer:

MethodDescriptionSpeedAccuracyDecentralization
OraclesOn-chain price feeds (e.g., API3, Chainlink)FastMediumMedium
DEX PoolsSwap events from decentralized exchangesFastMedium-HighHigh
Off-chain APIsExternal services (e.g., CoinGecko)SlowHighLow

Let's explore each method in detail.

Method 1: Using Oracle Price Feeds

Oracle networks provide on-chain price data through specialized smart contracts. For this tutorial, we'll use API3 price feeds on Blast.

How Oracles Work

Oracle services like API3 maintain a network of data providers that push price updates to on-chain contracts. These updates typically occur:

  • At regular time intervals
  • When price deviations exceed a predefined threshold (e.g., 1%)
  • When manually triggered by network participants

Finding the Right Oracle Feed

To locate the ETH/USD price feed using API3 on Blast:

  1. Identify the API3 contract address: 0x709944a48cAf83535e43471680fDA4905FB3920a

  2. Find the data feed ID for ETH/USD:

    • The dAPI name "ETH/USD" as bytes32: 0x4554482f55534400000000000000000000000000000000000000000000000000
    • Using the dapiNameToDataFeedId function, this maps to 0x3efb3990846102448c3ee2e47d22f1e5433cd45fa56901abe7ab3ffa054f70b5
  3. Monitor the UpdatedBeaconSetWithBeacons events with this data feed ID to get price updates

Oracle Advantages and Limitations

Advantages:

  • Fast indexing (no external API calls required)
  • Moderate decentralization
  • Generally reliable data

Limitations:

  • Updates only on significant price changes
  • Limited token coverage (mainly high-liquidity pairs)
  • Minor accuracy tradeoffs

Method 2: Using DEX Pool Swap Events

Decentralized exchanges like Uniswap provide price data through swap events. We'll use the USDB/WETH pool on Blast to derive ETH pricing.

Locating the Right DEX Pool

First, we need to find the specific Uniswap V3 pool for USDB/WETH:

import { createPublicClient, http, parseAbi, getContract } from "viem";
import { blast } from "viem/chains";

const usdb = "0x4300000000000000000000000000000000000003";
const weth = "0x4300000000000000000000000000000000000004";
const factoryAddress = "0x792edAdE80af5fC680d96a2eD80A44247D2Cf6Fd";
const factoryAbi = parseAbi([
"function getPool( address tokenA, address tokenB, uint24 fee ) external view returns (address pool)",
]);

const providerUrl = "https://rpc.ankr.com/blast";
const poolBips = 3000; // 0.3%. This is measured in hundredths of a bip

const client = createPublicClient({
chain: blast,
transport: http(providerUrl),
});

const factoryContract = getContract({
abi: factoryAbi,
address: factoryAddress,
client: client,
});

(async () => {
const poolAddress = await factoryContract.read.getPool([
usdb,
weth,
poolBips,
]);
console.log(poolAddress);
})();

Tip: You can also manually find the pool address using the getPool function on a block explorer.

Running this code reveals the USDB/WETH pool is at 0xf52B4b69123CbcF07798AE8265642793b2E8990C.

Getting Price Data From Swap Events

Uniswap V3 emits Swap events containing price information in the sqrtPriceX96 field. To convert this to a price, we'll use a formula in our event handler.

DEX Advantages and Limitations

Advantages:

  • Very decentralized
  • High update frequency
  • Wide token coverage

Limitations:

  • Susceptible to price impact and manipulation (especially in low-liquidity pools)
  • Requires extra calculations to derive prices
  • May require multiple pools for cross-pair calculations

Method 3: Using Off-chain APIs

External price APIs like CoinGecko provide comprehensive token price data but require HTTP calls from your indexer.

Making API Requests

Here's a simple function to fetch historical ETH prices from CoinGecko:

const COIN_GECKO_API_KEY = process.env.COIN_GECKO_API_KEY;

async function fetchEthPriceFromUnix(
unix: number,
token = "ethereum"
): Promise<number> {
// convert unix to date dd-mm-yyyy
const _date = new Date(unix * 1000);
const date = _date.toISOString().slice(0, 10).split("-").reverse().join("-");
return fetchEthPrice(date.slice(0, 10), token);
}

async function fetchEthPrice(
date: string,
token = "ethereum"
): Promise<number> {
const options = {
method: "GET",
headers: {
accept: "application/json",
"x-cg-demo-api-key": COIN_GECKO_API_KEY,
},
};

return fetch(
`https://api.coingecko.com/api/v3/coins/${token}/history?date=${date}&localization=false`,
options as any
)
.then((res) => res.json())
.then((res: any) => {
const usdPrice = res.market_data.current_price.usd;
console.log(`ETH price on ${date}: ${usdPrice}`);
return usdPrice;
})
.catch((err) => console.error(err));
}

export default fetchEthPriceFromUnix;

Note: The free CoinGecko API only provides daily price data (at 00:00 UTC), not block-by-block precision. For production use, consider a paid API with more granular historical data.

Off-chain API Advantages and Limitations

Advantages:

  • Highest accuracy (with paid APIs)
  • Most comprehensive token coverage
  • No susceptibility to on-chain manipulation

Limitations:

  • Significantly slows indexing speed due to API calls
  • Centralized data source
  • May require paid subscriptions for full functionality

Building a Multi-Source Price Feed Indexer

Now let's build an indexer that compares all three methods when tracking Uniswap V3 liquidity pool deposits.

Step 1: Initialize Your Indexer

Create a new Envio indexer project:

npx envio init

Step 2: Configure Your Indexer

Edit your config.yaml file to track both the API3 oracle and the Uniswap V3 pool:

# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: envio-indexer
rollback_on_reorg: false
networks:
- id: 81457
start_block: 11000000
contracts:
- name: Api3ServerV1
address:
- 0x709944a48cAf83535e43471680fDA4905FB3920a
handler: src/EventHandlers.ts
events:
- event: UpdatedBeaconSetWithBeacons(bytes32 indexed beaconSetId, int224 value, uint32 timestamp)
- name: UniswapV3Pool
address:
- 0xf52B4b69123CbcF07798AE8265642793b2E8990C
handler: src/EventHandlers.ts
events:
- event: Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
- event: Mint(address sender, address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1)
field_selection:
transaction_fields:
- "hash"

Important: The field_selection section is needed to include transaction hashes in your indexed data.

Step 3: Define Your Schema

Create a schema that captures price data from all three sources:

type OraclePoolPrice {
id: ID!
value: BigInt!
timestamp: BigInt!
block: Int!
}

type UniswapV3PoolPrice {
id: ID!
sqrtPriceX96: BigInt!
timestamp: Int!
block: Int!
}

type EthDeposited {
id: ID!
timestamp: Int!
block: Int!
oraclePrice: Float!
poolPrice: Float!
offChainPrice: Float!
offchainOracleDiff: Float!
depositedPool: Float!
depositedOffchain: Float!
depositedOrcale: Float!
txHash: String!
}

Step 4: Implement Event Handlers

Create event handlers to process data from all three sources:

import {
Api3ServerV1,
OraclePoolPrice,
UniswapV3Pool,
UniswapV3PoolPrice,
EthDeposited,
} from "generated";
import fetchEthPriceFromUnix from "./request";

let latestOraclePrice = 0;
let latestPoolPrice = 0;

Api3ServerV1.UpdatedBeaconSetWithBeacons.handler(async ({ event, context }) => {
// Filter out the beacon set for the ETH/USD price
if (
event.params.beaconSetId !=
"0x3efb3990846102448c3ee2e47d22f1e5433cd45fa56901abe7ab3ffa054f70b5"
) {
return;
}

const entity: OraclePoolPrice = {
id: `${event.chainId}-${event.block.number}-${event.logIndex}`,
value: event.params.value,
timestamp: event.params.timestamp,
block: event.block.number,
};

latestOraclePrice = Number(event.params.value) / Number(10 ** 18);

context.OraclePoolPrice.set(entity);
});

UniswapV3Pool.Swap.handler(async ({ event, context }) => {
const entity: UniswapV3PoolPrice = {
id: `${event.chainId}-${event.block.number}-${event.logIndex}`,
sqrtPriceX96: event.params.sqrtPriceX96,
timestamp: event.block.timestamp,
block: event.block.number,
};

latestPoolPrice = Number(
BigInt(2 ** 192) /
(BigInt(event.params.sqrtPriceX96) * BigInt(event.params.sqrtPriceX96))
);

context.UniswapV3PoolPrice.set(entity);
});

UniswapV3Pool.Mint.handler(async ({ event, context }) => {
const offChainPrice = await fetchEthPriceFromUnix(event.block.timestamp);

const ethDepositedUsdPool =
(latestPoolPrice * Number(event.params.amount1)) / 10 ** 18;
const ethDepositedUsdOffchain =
(offChainPrice * Number(event.params.amount1)) / 10 ** 18;
const ethDepositedUsdOrcale =
(latestOraclePrice * Number(event.params.amount1)) / 10 ** 18;

const EthDeposited: EthDeposited = {
id: `${event.chainId}-${event.block.number}-${event.logIndex}`,
timestamp: event.block.timestamp,
block: event.block.number,
oraclePrice: round(latestOraclePrice),
poolPrice: round(latestPoolPrice),
offChainPrice: round(offChainPrice),
depositedPool: round(ethDepositedUsdPool),
depositedOffchain: round(ethDepositedUsdOffchain),
depositedOrcale: round(ethDepositedUsdOrcale),
offchainOracleDiff: round(
((ethDepositedUsdOffchain - ethDepositedUsdOrcale) /
ethDepositedUsdOffchain) *
100
),
txHash: event.transaction.hash,
};

context.EthDeposited.set(EthDeposited);
});

function round(value: number) {
return Math.round(value * 100) / 100;
}

Step 5: Run Your Indexer

Start your indexer with:

pnpm dev

This will begin indexing data from block 11,000,000 on Blast.

Step 6: Analyze the Results

After running your indexer, you can query the data in Hasura to compare the three price data sources:

query ComparePrices {
EthDeposited(order_by: { block: desc }, limit: 10) {
block
timestamp
oraclePrice
poolPrice
offChainPrice
depositedPool
depositedOffchain
depositedOrcale
offchainOracleDiff
txHash
}
}

Results Analysis

When comparing our three price data sources, we found:

Table of indexer results

Looking at the offchainOracleDiff column, we can see that oracle and off-chain prices typically align closely but can deviate by as much as 17.98% in some cases.

For the highlighted transaction (0xe7e79ddf29ed2f0ea8cb5bb4ffdab1ea23d0a3a0a57cacfa875f0d15768ba37d), we can compare our calculated values:

  • Actual value (from block explorer): $2,358.27
  • DEX pool value (depositedPool): $2,117.07
  • Off-chain API value (depositedOffchain): $2,156.15

This demonstrates that even the most accurate methods have limitations.

Conclusion: Choosing the Right Method

Based on our analysis, here are some recommendations for choosing a price data method:

Use Oracle or DEX Pools when:

  • Indexing speed is critical
  • Absolute precision isn't required
  • You're working with high-liquidity tokens

Use Off-chain APIs when:

  • Price accuracy is paramount
  • Indexing speed is less important
  • You can implement effective caching

For maximum accuracy while maintaining performance:

  • Combine multiple methods and aggregate results
  • Use high-volume DEX pools on major networks
  • Cache API results to avoid redundant calls

Next Steps

To further enhance your price data indexing:

  1. Implement caching for off-chain API calls
  2. Cross-reference multiple DEX pools for better accuracy
  3. Consider time-weighted average prices (TWAP) instead of spot prices
  4. Use multi-chain indexing to access higher-liquidity pools on major networks

By carefully choosing and implementing the right price data strategy, you can build robust indexers that provide accurate financial data for your blockchain applications.