Skip to main content

Overview

For tax reporting, you need per-epoch reward data to calculate the fiat value of rewards at the time they were received. This requires iterating through epochs using the Custom Range Rewards approach.
API Endpoint: This guide uses /api/v2/ethereum/validators/rewards-list for per-epoch reward data required for tax calculations.
Disclaimer: This guide provides technical instructions for fetching reward data. It is not tax advice. Consult a qualified tax professional for guidance on how staking rewards should be reported in your jurisdiction.

Why Per-Epoch Data is Required

Most tax jurisdictions require you to report the fiat value of staking rewards at the time they were received. This means you need:
  1. Reward amount for each epoch (or day)
  2. Historical ETH-USD price at that time
  3. Calculation: fiat_value = reward_eth × price_at_time
The all_time or rolling period windows from Rewards Aggregated only provide a total sum—they cannot be used for tax calculations because you need per-epoch data to match with historical prices.

Tax Year Epoch Boundaries

First, calculate which epochs correspond to your tax year in your local time zone:
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 epoch_to_timestamp(epoch: int) -> int:
    """Convert epoch number to Unix timestamp."""
    return GENESIS_TIMESTAMP + (epoch * SECONDS_PER_EPOCH)

def get_tax_year_boundaries(year: int, tz_offset_hours: int = 0) -> dict:
    """
    Get epoch boundaries for a tax year in a specific time zone.
    
    Args:
        year: The tax year (e.g., 2023, 2024)
        tz_offset_hours: Time zone offset from UTC (e.g., 1 for CET, -5 for EST)
    """
    tz = timezone(timedelta(hours=tz_offset_hours))
    
    start_dt = datetime(year, 1, 1, 0, 0, 0, tzinfo=tz)
    end_dt = datetime(year, 12, 31, 23, 59, 59, tzinfo=tz)
    
    start_epoch = timestamp_to_epoch(int(start_dt.timestamp()))
    end_epoch = timestamp_to_epoch(int(end_dt.timestamp()))
    
    return {
        'year': year,
        'timezone': f"UTC{'+' if tz_offset_hours >= 0 else ''}{tz_offset_hours}",
        'start_epoch': start_epoch,
        'end_epoch': end_epoch,
        'total_epochs': end_epoch - start_epoch + 1,
        'start_date': start_dt.isoformat(),
        'end_date': end_dt.isoformat()
    }

# Example: Tax year 2024 in different time zones
for name, offset in [("UTC", 0), ("CET", 1), ("EST", -5)]:
    b = get_tax_year_boundaries(2024, offset)
    print(f"{name}: Epochs {b['start_epoch']:,} to {b['end_epoch']:,} ({b['total_epochs']:,} epochs)")
Output:
UTC: Epochs 253,238 to 335,587 (82,350 epochs)
CET: Epochs 253,229 to 335,578 (82,350 epochs)
EST: Epochs 253,251 to 335,600 (82,350 epochs)

Calculating Tax Year Rewards with Price Data

Use the Custom Range Rewards approach to iterate through epochs and collect per-epoch rewards that can be matched with historical prices:
import requests
import time
from datetime import datetime, timezone, timedelta
from typing import Generator, List, Dict

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 epoch_to_timestamp(epoch: int) -> int:
    return GENESIS_TIMESTAMP + (epoch * SECONDS_PER_EPOCH)

def get_tax_year_boundaries(year: int, tz_offset_hours: int = 0) -> dict:
    tz = timezone(timedelta(hours=tz_offset_hours))
    start_dt = datetime(year, 1, 1, 0, 0, 0, tzinfo=tz)
    end_dt = datetime(year, 12, 31, 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) -> Generator[dict, None, None]:
    """Fetch all validator rewards for a single epoch."""
    cursor = None
    while True:
        payload = {
            "chain": "mainnet",
            "validator": {"dashboard_id": dashboard_id},
            "epoch": epoch,
            "page_size": 100
        }
        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_tax_year_rewards_with_prices(
    dashboard_id: int,
    year: int,
    tz_offset_hours: int = 0,
    get_eth_price: callable = None  # Function to get ETH price for a timestamp
) -> List[Dict]:
    """
    Calculate rewards for each epoch in a tax year with fiat conversion.
    
    Returns a list of daily aggregated rewards with fiat values.
    """
    boundaries = get_tax_year_boundaries(year, tz_offset_hours)
    start_epoch = boundaries['start_epoch']
    end_epoch = boundaries['end_epoch']
    
    # Aggregate rewards by day for practical tax reporting
    daily_rewards = {}
    
    for epoch in range(start_epoch, end_epoch + 1):
        epoch_ts = epoch_to_timestamp(epoch)
        date_key = datetime.fromtimestamp(
            epoch_ts, 
            tz=timezone(timedelta(hours=tz_offset_hours))
        ).strftime('%Y-%m-%d')
        
        if date_key not in daily_rewards:
            daily_rewards[date_key] = {
                'date': date_key,
                'total_wei': 0,
                'epochs': []
            }
        
        epoch_total = 0
        for reward in fetch_epoch_rewards(dashboard_id, epoch):
            epoch_total += int(reward.get("total", 0))
        
        daily_rewards[date_key]['total_wei'] += epoch_total
        daily_rewards[date_key]['epochs'].append(epoch)
    
    # Convert to list and add fiat values
    result = []
    for date_key in sorted(daily_rewards.keys()):
        day_data = daily_rewards[date_key]
        eth_amount = day_data['total_wei'] / 1e18
        
        # Get ETH price for this date (you need to implement this)
        eth_price = get_eth_price(date_key) if get_eth_price else None
        fiat_value = eth_amount * eth_price if eth_price else None
        
        result.append({
            'date': date_key,
            'reward_eth': eth_amount,
            'eth_price_usd': eth_price,
            'fiat_value_usd': fiat_value,
            'epoch_count': len(day_data['epochs'])
        })
    
    return result

# Example usage (you need to provide your own price data source)
def get_historical_eth_price(date_str: str) -> float:
    """
    Implement this function to fetch historical ETH-USD price.
    You might use CoinGecko, CryptoCompare, or your preferred data source.
    """
    # Example: return cached/API price for the date
    # return some_price_api.get_price('ETH', 'USD', date_str)
    return 2500.00  # Placeholder

if __name__ == "__main__":
    DASHBOARD_ID = 123
    
    results = calculate_tax_year_rewards_with_prices(
        dashboard_id=DASHBOARD_ID,
        year=2024,
        tz_offset_hours=1,  # CET
        get_eth_price=get_historical_eth_price
    )
    
    total_fiat = sum(r['fiat_value_usd'] or 0 for r in results)
    total_eth = sum(r['reward_eth'] for r in results)
    
    print(f"Tax Year 2024 Summary")
    print(f"Total ETH: {total_eth:.6f}")
    print(f"Total USD: ${total_fiat:,.2f}")
    print(f"\nFirst 5 days:")
    for r in results[:5]:
        print(f"  {r['date']}: {r['reward_eth']:.6f} ETH = ${r['fiat_value_usd']:,.2f}")
API Usage: A full tax year contains ~82,000 epochs. Each epoch requires a separate API call. See Custom Range Rewards for performance estimates and optimization tips.

Obtaining Historical ETH Prices

You’ll need historical ETH-USD prices for each day. Common sources include:
SourceNotes
CoinGecko APIFree tier available, daily historical prices
CryptoCompareHourly historical data available
KrakenExchange prices, high accuracy
Tax softwareMany crypto tax tools include price data
For tax compliance, use a consistent price source throughout your calculations and document which source you used.

Common Tax Year Time Zone Offsets

RegionTime ZoneUTC OffsetNotes
UKGMT/BST0 / +1BST during summer
Central EuropeCET/CEST+1 / +2CEST during summer
US EasternEST/EDT-5 / -4EDT during summer
US PacificPST/PDT-8 / -7PDT during summer
JapanJST+9No daylight saving
Australia EasternAEST/AEDT+10 / +11AEDT during summer
For tax purposes, use your local time zone’s standard offset (non-daylight-saving) or consult with your tax authority about which time zone to use. Some jurisdictions may require UTC.

PRO Feature: With a Scale or Enterprise plan, you can query by withdrawal address to capture all validators, and benefit from significantly higher rate limits. Use dashboards with groups to organize validators and query them efficiently with a single dashboard_id.