DEX daily prices API: OHLC and VWAP for any token

Get daily DEX OHLC prices and VWAP for a token with one GET /{chain}/dex/prices request, plus exact fractions in TypeScript and Python and backfills.

GET /{chain}/dex/prices returns daily DEX open, high, low, close and VWAP prices for one token for a date range. Developers and AI agents call it with the same API key.

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/dex/prices?token=0x1Cdad396DB64BDa184d5182A97Dd9B3C62100b7D&from=2026-09-01&to=2026-09-30" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

What the DEX daily prices dataset is

BlockVectra's DEX dataset indexes decentralized exchange trade activity and computes aggregated daily pricing metrics. The DEX daily prices endpoint (getDexPrices) provides the daily volume-weighted average price (VWAP), price indicators (fields first_price, last_price, min_price, and max_price), and volume metrics for a specified token over a given date range.

Availability of this dataset varies across networks; chains providing this dataset are subject to the Supported Chains page.

This endpoint is not paginated: all matching daily rows within the requested date span are returned directly in data, and next_cursor is never present. If your application requires granular swap transactions rather than daily aggregates, use GET /{chain}/dex/swaps (see the Data API Reference).

Request parameters and limits

The endpoint route is GET https://api.blockvectra.com/v1/data/{chain}/dex/prices. All requests require authentication by supplying your API key in the x-api-key header.

The endpoint accepts the following query parameters:

ParameterLocationTypeRequiredDescription
chainpathstringYesChain identifier, e.g. robinhood_mainnet
tokenquerystringYes20-byte base token address, 0x optional, either case
quotequerystringNoOptional 20-byte quote token address to restrict to a single base/quote pair
fromquerydate stringYesUTC start date, inclusive, YYYY-MM-DD
toquerydate stringYesUTC end date, inclusive, YYYY-MM-DD. to - from must be <= 90 days

Constraints and error codes

When a request violates constraints, the API returns a structured error body {"error":{"code","message"}}:

  • HTTP 400 (bad_request): Missing required query parameters (token, from, or to), invalid token/quote address syntax, invalid YYYY-MM-DD calendar dates, or from is after to.
  • HTTP 409 (span_exceeded): to - from is more than 90 days.
  • HTTP 404 (error.code: not_found, error.data.reason: unknown_chain): {chain} is unknown or not public in GET /chains; chain lookup precedes query validation.
  • HTTP 422 (no_coverage): The chain does not support the dex_prices dataset capability.
  • HTTP 503 (unavailable): Service temporarily unavailable; retry according to the Retry-After header.

Request examples

The following examples query daily DEX prices for a base token across September 2026:

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/dex/prices?token=0x1Cdad396DB64BDa184d5182A97Dd9B3C62100b7D&from=2026-09-01&to=2026-09-30" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

Detailed field reference

Each entry in data represents aggregated daily DEX metrics for the token pair on that UTC date:

Token and quote assets

  • day (string): UTC date in YYYY-MM-DD format.
  • token (string): 20-byte base token address in lowercase 0x-prefixed hex.
  • token_symbol (string or null): Base token symbol.
  • token_name (string or null): Base token display name.
  • quote_token (string): Quote asset address. The all-zero address (0x0000000000000000000000000000000000000000) represents native ETH as the quote asset.
  • quote_symbol (string or null): Quote asset symbol ("ETH" when quote_token is the all-zero address).
  • quote_name (string or null): Quote asset display name ("Ether" when quote_token is the all-zero address).
  • base_decimals (integer or null): Base token decimals (0–255).
  • quote_decimals (integer or null): Quote asset decimals (18 when quote_token is the all-zero address).

Volume and trade counts

  • swap_count (integer): Swap count for this row.
  • base_volume_raw (string): Atomic base volume as an unsigned integer decimal string (UInt256String).
  • quote_volume_raw (string): Atomic quote volume as an unsigned integer decimal string (UInt256String).
  • base_volume (string or null): Human-readable base token volume scaled by base_decimals as a DecimalString; null when base_decimals is unknown.
  • quote_volume (string or null): Quote volume scaled by quote decimals as a DecimalString, or null.

Price indicators and VWAP

  • vwap (string or null): Volume-weighted average price as a DecimalString, or null.
  • first_price (string or null): First price indicator as a DecimalString, or null.
  • last_price (string or null): Last price indicator as a DecimalString, or null.
  • min_price (string or null): Minimum price indicator as a DecimalString, or null.
  • max_price (string or null): Maximum price indicator as a DecimalString, or null.

Exact fraction fields

  • first_price_numerator / first_price_denominator (string): Raw integer amounts of the swap behind first_price (UInt256String): the numerator is quote_amount_raw in quote atomic units and the denominator is base_amount_raw in base atomic units. They are not the scaled human-readable first_price as an exact fraction.
  • last_price_numerator / last_price_denominator (string): Raw integer amounts of the swap behind last_price (UInt256String): the numerator is quote_amount_raw in quote atomic units and the denominator is base_amount_raw in base atomic units. They are not the scaled human-readable last_price as an exact fraction.
  • min_price_numerator / min_price_denominator (string): Raw integer amounts of the swap behind min_price (UInt256String): the numerator is quote_amount_raw in quote atomic units and the denominator is base_amount_raw in base atomic units. They are not the scaled human-readable min_price as an exact fraction.
  • max_price_numerator / max_price_denominator (string): Raw integer amounts of the swap behind max_price (UInt256String): the numerator is quote_amount_raw in quote atomic units and the denominator is base_amount_raw in base atomic units. They are not the scaled human-readable max_price as an exact fraction.
  • refreshed_at (string): Refresh timestamp for this row (ISO-8601 UTC timestamp).

The raw ratio numerator / denominator is quote atomic units per base atomic unit. The human-readable price (quote tokens per base token) is the raw ratio × 10^(base_decimals − quote_decimals). When base_decimals or quote_decimals is null, the human-readable price is unknown; do not assume 18. 0 is a valid decimals value. When a denominator is 0, do not divide; treat the human-readable price as null. For the same pair (same decimals), price direction and relative change can be computed directly from the raw ratios.

Envelope metadata (meta)

  • chain: Chain identifier.
  • chain_slug: Canonical uppercase chain slug.
  • chain_external_id: CAIP-2 formatted chain identifier.
  • as_of_block: The chain's newest fully written block (reported by this dataset, not checked against request parameters).
  • coverage: Coverage classification (reports "full" for this endpoint).
  • refreshed_at: Metadata refreshed timestamp. May be null: null means the update time of this data is unknown and it should be treated as stale; block-based endpoints always return a value.

Why prices use exact numerators and denominators

Standard JSON numbers rely on IEEE-754 double-precision floats, which present precision limitations:

  1. Floating-point truncation and drift: Float64 values provide only 53 bits of precision, and dividing token quantities yields rounding drift that compounds across calculations.
  2. Transport safety: Formatting values as decimal strings (UInt256String) ensures numbers travel across HTTP without losing precision in JSON parsers.

By providing the exact integer numerator and denominator for price indicators, BlockVectra enables exact mathematical calculations without floating-point conversion.

Handling exact fractions in TypeScript (BigInt)

In TypeScript, native BigInt handles ratio comparison and decimals scaling without floating-point conversion. isPriceUp compares the raw ratios of one pair; rawRatioToHumanString applies the decimals scaling and returns null when decimals are unknown or the denominator is 0. Its output is truncated (not rounded) to displayDigits digits: this is display truncation, separate from the token's own precision, and does not claim an exact finite decimal expansion.

interface DexDailyPrice {
  base_decimals: number | null;
  quote_decimals: number | null;
  // Raw amounts: numerator is quote_amount_raw, denominator is base_amount_raw.
  first_price_numerator: string;
  first_price_denominator: string;
  last_price_numerator: string;
  last_price_denominator: string;
}

// 1. Raw ratios of one pair share the same decimals, so they compare directly.
// a / b > c / d  is equivalent to  a * d > c * b  (denominators must be non-zero)
export function isPriceUp(row: DexDailyPrice): boolean | null {
  const openNum = BigInt(row.first_price_numerator);
  const openDen = BigInt(row.first_price_denominator);
  const closeNum = BigInt(row.last_price_numerator);
  const closeDen = BigInt(row.last_price_denominator);
  if (openDen === 0n || closeDen === 0n) return null;

  return closeNum * openDen > openNum * closeDen;
}

// 2. Raw ratio -> human-readable price (quote tokens per base token):
//    human = numerator / denominator * 10^(baseDecimals - quoteDecimals)
// Returns null when a decimals value is null (0 is valid) or the denominator is 0.
// The result is truncated to displayDigits digits for display only.
export function rawRatioToHumanString(
  numeratorStr: string,
  denominatorStr: string,
  baseDecimals: number | null,
  quoteDecimals: number | null,
  displayDigits = 18
): string | null {
  if (baseDecimals === null || quoteDecimals === null) return null;
  let num = BigInt(numeratorStr);
  let den = BigInt(denominatorStr);
  if (den === 0n) return null;
  const shift = baseDecimals - quoteDecimals;
  if (shift >= 0) {
    num *= 10n ** BigInt(shift);
  } else {
    den *= 10n ** BigInt(-shift);
  }
  const unit = 10n ** BigInt(displayDigits);
  const scaled = (num * unit) / den;
  const intPart = scaled / unit;
  if (displayDigits === 0) return intPart.toString();
  const fracPart = (scaled % unit).toString().padStart(displayDigits, "0");

  return `${intPart}.${fracPart}`;
}

Handling exact fractions in Python

Python's standard library provides fractions.Fraction for exact rational arithmetic. human_price applies the decimals scaling exactly and returns None when decimals are unknown or the denominator is 0; format_fraction rounds half to even with integer arithmetic, only for display.

from fractions import Fraction


def raw_ratio(numerator, denominator):
    """Raw ratio: quote atomic units per base atomic unit. None when the denominator is 0."""
    den = int(denominator)
    if den == 0:
        return None
    return Fraction(int(numerator), den)


def human_price(numerator, denominator, base_decimals, quote_decimals):
    """Quote tokens per base token = raw ratio * 10^(base_decimals - quote_decimals).

    Returns None when a decimals value is None (0 is valid) or the denominator is 0.
    """
    if base_decimals is None or quote_decimals is None:
        return None
    raw = raw_ratio(numerator, denominator)
    if raw is None:
        return None
    return raw * Fraction(10) ** (base_decimals - quote_decimals)


def percentage_change(row):
    """Same pair, same decimals: raw ratios compare directly. Exact Fraction, no float."""
    open_raw = raw_ratio(row["first_price_numerator"], row["first_price_denominator"])
    close_raw = raw_ratio(row["last_price_numerator"], row["last_price_denominator"])
    if open_raw is None or close_raw is None or open_raw == 0:
        return None
    return (close_raw - open_raw) / open_raw * 100


def format_fraction(value, digits=18):
    """Display only: round half to even at `digits` places with exact integer arithmetic."""
    quotient, remainder = divmod(abs(value.numerator) * 10**digits, value.denominator)
    twice = remainder * 2
    if twice > value.denominator or (twice == value.denominator and quotient % 2 == 1):
        quotient += 1
    whole, fraction = divmod(quotient, 10**digits)
    text = str(whole) if digits == 0 else f"{whole}.{fraction:0{digits}d}"
    return f"-{text}" if value < 0 and quotient != 0 else text

Backfilling one year of daily prices

To backfill a year of data (365 days) within the 90-day span limit, divide the full date range into consecutive windows of at most 90 days and issue chunked requests:

interface DateSpan {
  from: string;
  to: string;
}

/**
 * Split a large date range into consecutive spans of at most maxDays (default: 90)
 */
export function splitDateRange(startDateStr: string, endDateStr: string, maxDays = 90): DateSpan[] {
  const spans: DateSpan[] = [];
  let currentStart = new Date(startDateStr);
  const end = new Date(endDateStr);

  while (currentStart <= end) {
    const chunkEnd = new Date(currentStart);
    chunkEnd.setUTCDate(chunkEnd.getUTCDate() + (maxDays - 1));
    const effectiveEnd = chunkEnd < end ? chunkEnd : end;

    spans.push({
      from: currentStart.toISOString().slice(0, 10),
      to: effectiveEnd.toISOString().slice(0, 10),
    });

    const nextStart = new Date(effectiveEnd);
    nextStart.setUTCDate(nextStart.getUTCDate() + 1);
    currentStart = nextStart;
  }

  return spans;
}

/**
 * Backfill token daily prices across multiple 90-day chunks
 */
export async function backfillTokenDailyPrices(
  chain: string,
  token: string,
  startDate: string,
  endDate: string,
  apiKey: string
) {
  const chunks = splitDateRange(startDate, endDate, 90);
  const allDailyPrices = [];

  for (const chunk of chunks) {
    const url = new URL(`https://api.blockvectra.com/v1/data/${chain}/dex/prices`);
    url.searchParams.set("token", token);
    url.searchParams.set("from", chunk.from);
    url.searchParams.set("to", chunk.to);

    const res = await fetch(url, {
      headers: { "x-api-key": apiKey },
    });

    if (!res.ok) {
      throw new Error(`Failed to fetch span ${chunk.from}..${chunk.to}: HTTP ${res.status}`);
    }

    const json = await res.json();
    allDailyPrices.push(...json.data);
  }

  return allDailyPrices;
}

Capacity and CU usage calculations

Every Data API endpoint meters consumption in Compute Units (CU). The per-call CU weight for data.dex_prices and the estimated consumption for token backfills are calculated below:

Method Weightdata.dex_prices: 15 CU / call
  • Backfilling 1 year of daily prices for 200 tokens: with a max span of 90 days per request, covering 365 days takes 5 chunks per token, totaling 1,000 calls. Total consumption is 15,000 CU (approx <0.1% of the free plan cycle allowance), about <$0.01 at list price.
  • Daily maintenance (refreshing 200 tokens once per day): 200 calls/day (3,000 CU/day), totaling approximately 6,000 calls per 30-day cycle (90,000 CU, approx 0.3% of the free quota), about <$0.01/month at list price.

When scaling your backfill volume or requiring higher request concurrency, top up on-chain on the console Billing page to upgrade to a paid account. For active rates and unit conversions, see the Pricing page.

Next steps

Last updated:

On this page