Skip to main content

Overview

When you need rewards for a specific date range that doesn’t match the predefined evaluation windows (24h, 7d, 30d, 90d, all_time), you must iterate through each epoch using the Rewards List endpoint.
API Endpoint: This guide uses /api/v2/ethereum/validators/rewards-list for per-epoch reward data.
The Rewards Aggregated endpoint only supports rolling periods and all_time. For custom historical ranges (like “March 15 to June 30, 2024”), you must use the Rewards List endpoint.

When to Use Custom Range Calculation

ScenarioApproach
Last 30 days summary (total only)Use rewards-aggregate with 30d
All-time rewards (total only)Use rewards-aggregate with all_time
Specific month (e.g., March 2024)Iterate epochs with rewards-list ⚠️
Tax reporting (any year)Iterate epochs with rewards-list ⚠️
Q1 rewards (Jan 1 - Mar 31)Iterate epochs with rewards-list ⚠️
Per-epoch data for fiat conversionIterate epochs with rewards-list ⚠️
Tax Calculations: Even if your validators started during the tax year, you still need per-epoch data to calculate fiat values at the time rewards were received. The all_time aggregate cannot be used for tax purposes. See Tax Year Calculations for complete tax guidance.

Converting Date Ranges to Epochs

First, calculate which epochs correspond to your desired date range:
from datetime import datetime, timezone, timedelta

GENESIS_TIMESTAMP = 1606824023  # Dec 1, 2020, 12:00:23 UTC
SECONDS_PER_EPOCH = 384         # 32 slots × 12 seconds

def timestamp_to_epoch(timestamp: int) -> int:
    """Convert Unix timestamp to Ethereum epoch number."""
    return (timestamp - GENESIS_TIMESTAMP) // SECONDS_PER_EPOCH

def get_date_range_epochs(
    start_date: tuple,  # (year, month, day)
    end_date: tuple,    # (year, month, day)
    tz_offset_hours: int = 0
) -> dict:
    """
    Get epoch boundaries for a custom date range.
    
    Args:
        start_date: Tuple of (year, month, day) for range start
        end_date: Tuple of (year, month, day) for range end
        tz_offset_hours: Time zone offset from UTC
    """
    tz = timezone(timedelta(hours=tz_offset_hours))
    
    # Start of first day
    start_dt = datetime(start_date[0], start_date[1], start_date[2], 0, 0, 0, tzinfo=tz)
    # End of last day (23:59:59)
    end_dt = datetime(end_date[0], end_date[1], end_date[2], 23, 59, 59, tzinfo=tz)
    
    start_epoch = timestamp_to_epoch(int(start_dt.timestamp()))
    end_epoch = timestamp_to_epoch(int(end_dt.timestamp()))
    
    return {
        'start_date': start_dt.isoformat(),
        'end_date': end_dt.isoformat(),
        'start_epoch': start_epoch,
        'end_epoch': end_epoch,
        'total_epochs': end_epoch - start_epoch + 1
    }

# Examples
print("Q1 2024 (UTC):")
q1 = get_date_range_epochs((2024, 1, 1), (2024, 3, 31), 0)
print(f"  Epochs {q1['start_epoch']:,} to {q1['end_epoch']:,} ({q1['total_epochs']:,} epochs)")

print("\nMarch 2024 (CET):")
march = get_date_range_epochs((2024, 3, 1), (2024, 3, 31), 1)
print(f"  Epochs {march['start_epoch']:,} to {march['end_epoch']:,} ({march['total_epochs']:,} epochs)")
Output:
Q1 2024 (UTC):
  Epochs 253,238 to 273,600 (20,363 epochs)

March 2024 (CET):
  Epochs 266,839 to 273,591 (6,753 epochs)

Iterating Through Epochs

Use the Rewards List endpoint to fetch rewards for each epoch in your range.
API Usage Consideration: Each epoch requires a separate API call. A full month contains ~6,750 epochs, and a full year contains ~82,000 epochs. Ensure your API plan supports this volume of requests.

Complete Custom Range Calculator

import requests
import time
from datetime import datetime, timezone, timedelta
from typing import Generator

GENESIS_TIMESTAMP = 1606824023
SECONDS_PER_EPOCH = 384

API_KEY = "<YOUR_API_KEY>"
API_BASE = "https://beaconcha.in/api/v2/ethereum/validators"

def timestamp_to_epoch(timestamp: int) -> int:
    return (timestamp - GENESIS_TIMESTAMP) // SECONDS_PER_EPOCH

def get_date_range_epochs(start_date: tuple, end_date: tuple, tz_offset_hours: int = 0) -> dict:
    tz = timezone(timedelta(hours=tz_offset_hours))
    start_dt = datetime(start_date[0], start_date[1], start_date[2], 0, 0, 0, tzinfo=tz)
    end_dt = datetime(end_date[0], end_date[1], end_date[2], 23, 59, 59, tzinfo=tz)
    return {
        'start_epoch': timestamp_to_epoch(int(start_dt.timestamp())),
        'end_epoch': timestamp_to_epoch(int(end_dt.timestamp()))
    }

def fetch_epoch_rewards(dashboard_id: int, epoch: int, page_size: int = 100) -> Generator[dict, None, None]:
    """Fetch all validator rewards for a single epoch with pagination."""
    cursor = None
    
    while True:
        payload = {
            "chain": "mainnet",
            "validator": {"dashboard_id": dashboard_id},
            "epoch": epoch,
            "page_size": page_size
        }
        if cursor:
            payload["cursor"] = cursor
        
        response = requests.post(
            f"{API_BASE}/rewards-list",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json"
            },
            json=payload
        )
        response.raise_for_status()
        data = response.json()
        
        for reward in data.get("data", []):
            yield reward
        
        cursor = data.get("paging", {}).get("next_cursor")
        if not cursor:
            break

def calculate_custom_range_rewards(
    dashboard_id: int,
    start_date: tuple,  # (year, month, day)
    end_date: tuple,    # (year, month, day)
    tz_offset_hours: int = 0,
    progress_interval: int = 500
) -> dict:
    """
    Calculate total rewards for a custom date range.
    
    Args:
        dashboard_id: Your beaconcha.in dashboard ID
        start_date: Tuple of (year, month, day) for range start
        end_date: Tuple of (year, month, day) for range end
        tz_offset_hours: Time zone offset from UTC
        progress_interval: Print progress every N epochs
    """
    boundaries = get_date_range_epochs(start_date, end_date, tz_offset_hours)
    start_epoch = boundaries['start_epoch']
    end_epoch = boundaries['end_epoch']
    total_epochs = end_epoch - start_epoch + 1
    
    print(f"Calculating rewards for {start_date} to {end_date}")
    print(f"Epoch range: {start_epoch:,} to {end_epoch:,} ({total_epochs:,} epochs)")
    print("-" * 60)
    
    total_wei = 0
    total_reward_wei = 0
    total_penalty_wei = 0
    epochs_processed = 0
    
    start_time = time.time()
    
    for epoch in range(start_epoch, end_epoch + 1):
        for reward in fetch_epoch_rewards(dashboard_id, epoch):
            total_wei += int(reward.get("total", 0))
            total_reward_wei += int(reward.get("total_reward", 0))
            total_penalty_wei += int(reward.get("total_penalty", 0))
        
        epochs_processed += 1
        
        if epochs_processed % progress_interval == 0:
            elapsed = time.time() - start_time
            rate = epochs_processed / elapsed
            remaining = (total_epochs - epochs_processed) / rate
            print(f"Progress: {epochs_processed:,}/{total_epochs:,} epochs "
                  f"({epochs_processed/total_epochs*100:.1f}%) - "
                  f"ETA: {remaining/60:.1f} min")
    
    elapsed = time.time() - start_time
    
    return {
        'start_date': f"{start_date[0]}-{start_date[1]:02d}-{start_date[2]:02d}",
        'end_date': f"{end_date[0]}-{end_date[1]:02d}-{end_date[2]:02d}",
        'start_epoch': start_epoch,
        'end_epoch': end_epoch,
        'epochs_processed': epochs_processed,
        'processing_time_seconds': elapsed,
        'total_wei': total_wei,
        'total_eth': total_wei / 1e18,
        'total_reward_wei': total_reward_wei,
        'total_reward_eth': total_reward_wei / 1e18,
        'total_penalty_wei': total_penalty_wei,
        'total_penalty_eth': total_penalty_wei / 1e18
    }

# Example: Calculate Q1 2024 rewards
if __name__ == "__main__":
    DASHBOARD_ID = 123  # Your dashboard ID
    
    result = calculate_custom_range_rewards(
        dashboard_id=DASHBOARD_ID,
        start_date=(2024, 1, 1),
        end_date=(2024, 3, 31),
        tz_offset_hours=0  # UTC
    )
    
    print(f"\n{'='*60}")
    print(f"Custom Range Summary: {result['start_date']} to {result['end_date']}")
    print(f"{'='*60}")
    print(f"Epochs: {result['start_epoch']:,} to {result['end_epoch']:,}")
    print(f"Processing time: {result['processing_time_seconds']/60:.1f} minutes")
    print(f"Net Rewards: {result['total_eth']:.6f} ETH")
    print(f"Gross Rewards: {result['total_reward_eth']:.6f} ETH")
    print(f"Penalties: {result['total_penalty_eth']:.6f} ETH")

Performance Considerations

The following estimates compare processing time between the Free tier (1 req/s) and Scale plan (5 req/s):
RangeEpochsEst. API CallsEst. Time (Free)Est. Time (Scale)
1 day~225~225~4-5 min~1 min
1 week~1,575~1,575~25-30 min~5-6 min
1 month~6,750~6,750~2 hours~25 min
1 quarter~20,250~20,250~6 hours~1 hour
1 year~82,000~82,000~23 hours~4-5 hours
Optimize API Usage: For large ranges, consider:
  • Use dashboards and groups — Query all validators in a dashboard with a single dashboard_id or filter by group_id, reducing the need for multiple requests per epoch
  • Run calculations during off-peak hours — Better API response times
  • Use parallel requests — Respecting your plan’s rate limits
Upgrade for Faster Processing: Scale offers 5x the rate limit of the free tier, while Enterprise plans offer custom limits for high-volume needs.

Use Case: Monthly Reporting

Calculate rewards for each month and generate a summary:
def calculate_monthly_rewards(dashboard_id: int, year: int, tz_offset_hours: int = 0):
    """Calculate rewards for each month of a year."""
    months = [
        (1, 31), (2, 28 if year % 4 else 29), (3, 31), (4, 30),
        (5, 31), (6, 30), (7, 31), (8, 31),
        (9, 30), (10, 31), (11, 30), (12, 31)
    ]
    
    results = []
    for month, last_day in months:
        result = calculate_custom_range_rewards(
            dashboard_id=dashboard_id,
            start_date=(year, month, 1),
            end_date=(year, month, last_day),
            tz_offset_hours=tz_offset_hours
        )
        results.append({
            'month': f"{year}-{month:02d}",
            'rewards_eth': result['total_eth']
        })
        print(f"{year}-{month:02d}: {result['total_eth']:.6f} ETH")
    
    return results

For detailed API specifications, see the Rewards (CL + EL) section in the V2 API Docs sidebar.