Web Scraping for Energy & Utilities: How AI Agents Track Prices, Grid Data, Regulations & Market Intelligence in 2026

Published: March 13, 2026 ยท 20 min read ยท By the Mantis Team

The global energy market exceeds $10 trillion annually, with US electricity generation alone worth over $400 billion and natural gas markets adding another $150 billion. Deregulated electricity markets like ERCOT (Texas), PJM (Mid-Atlantic), and CAISO (California) generate billions of price data points daily as locational marginal prices (LMPs) fluctuate every five minutes across thousands of nodes. Renewable energy capacity additions surpassed 500 GW globally in 2025, creating massive new data streams around solar irradiance, wind generation, and battery storage dispatch.

Enterprise energy analytics platforms like S&P Global Platts, Wood Mackenzie, and Genscape (now part of Wood Mackenzie) charge $5,000โ€“$50,000+ per month for market data, price assessments, and analytics. But the underlying data โ€” ISO/RTO real-time prices, FERC regulatory filings, EIA production statistics, utility rate tariffs, and renewable energy auction results โ€” is publicly available across dozens of government and market operator websites. AI agents powered by web scraping APIs can build equivalent intelligence at a fraction of the cost.

In this guide, you'll build a complete energy intelligence system using Python, the Mantis WebPerception API, and GPT-4o โ€” covering wholesale price monitoring, grid condition tracking, regulatory intelligence, and renewable energy market analysis.

Why Energy Organizations Need Web Scraping

Energy markets move fast. Wholesale electricity prices can spike 10,000% in minutes during grid emergencies (Texas's ERCOT hit $9,000/MWh during Winter Storm Uri). Regulatory decisions worth billions are buried in FERC docket filings. Utility rate changes affect millions of customers. The organizations that win are those with real-time intelligence:

Build Energy Intelligence Agents with Mantis

Scrape wholesale prices, grid data, regulatory filings, and utility rates from any energy platform with one API call. AI-powered extraction handles every data format.

Get Free API Key โ†’

Architecture: The 6-Step Energy Intelligence Pipeline

  1. Wholesale price scraping โ€” Monitor real-time and day-ahead LMPs, natural gas spot prices, and commodity benchmarks across markets
  2. Grid & generation tracking โ€” Track system load, generation mix (solar/wind/gas/nuclear), frequency, and congestion patterns
  3. Regulatory filing monitoring โ€” Scrape FERC eLibrary, state PUC dockets, and interconnection queues for new filings
  4. Utility rate intelligence โ€” Monitor retail rate changes, TOU schedules, demand charges, and net metering policies across utilities
  5. GPT-4o market analysis โ€” Identify price anomalies, regulatory risks, arbitrage opportunities, and renewable energy trends
  6. Alert delivery โ€” Route price spikes, grid emergencies, regulatory deadlines, and market opportunities to trading and strategy teams via Slack

Step 1: Define Your Energy Data Models

from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from enum import Enum

class MarketRegion(str, Enum):
    ERCOT = "ercot"      # Texas
    PJM = "pjm"          # Mid-Atlantic, Midwest
    CAISO = "caiso"       # California
    NYISO = "nyiso"       # New York
    MISO = "miso"         # Central US
    SPP = "spp"           # Southwest Power Pool
    ISONE = "isone"       # New England
    AESO = "aeso"         # Alberta, Canada

class PriceType(str, Enum):
    REAL_TIME = "real_time"      # 5-min LMP
    DAY_AHEAD = "day_ahead"      # hourly DA LMP
    BILATERAL = "bilateral"
    FUTURES = "futures"
    SPOT = "spot"

class FuelType(str, Enum):
    NATURAL_GAS = "natural_gas"
    COAL = "coal"
    NUCLEAR = "nuclear"
    WIND = "wind"
    SOLAR = "solar"
    HYDRO = "hydro"
    BATTERY = "battery"
    OTHER = "other"

class EnergyPrice(BaseModel):
    """Wholesale electricity or commodity price point."""
    market: MarketRegion
    price_type: PriceType
    node_or_zone: str            # "HB_HOUSTON", "SP15", "WESTERN_HUB"
    price_usd_mwh: float         # $/MWh for electricity
    congestion_component: Optional[float]
    loss_component: Optional[float]
    energy_component: Optional[float]
    timestamp: datetime           # interval start time
    interval_minutes: int = 5     # 5 for RT, 60 for DA
    volume_mw: Optional[float]
    is_peak: bool                 # on-peak vs off-peak
    scraped_at: datetime

class GridStatus(BaseModel):
    """Real-time grid operating conditions."""
    market: MarketRegion
    system_load_mw: float
    forecast_load_mw: Optional[float]
    total_generation_mw: float
    generation_by_fuel: dict      # {"wind": 18500, "solar": 12000, ...}
    wind_penetration_pct: Optional[float]
    solar_penetration_pct: Optional[float]
    renewable_curtailment_mw: Optional[float]
    frequency_hz: Optional[float]  # target: 60.000 Hz
    net_interchange_mw: Optional[float]  # imports (+) / exports (-)
    operating_reserves_mw: Optional[float]
    alert_level: Optional[str]    # "normal", "watch", "warning", "emergency", "EEA1", "EEA2", "EEA3"
    timestamp: datetime
    scraped_at: datetime

class RegulatoryFiling(BaseModel):
    """FERC or state PUC regulatory filing."""
    docket_number: str           # "ER25-1234-000"
    filing_type: str             # "rate_case", "interconnection", "merger", "tariff_change"
    title: str
    filer: str                   # company name
    agency: str                  # "FERC", "CPUC", "ERCOT", "PUCT"
    status: str                  # "pending", "approved", "rejected", "withdrawn"
    filed_date: str
    due_date: Optional[str]      # comment/protest deadline
    description: str
    estimated_impact_usd: Optional[float]
    affected_states: List[str]
    document_urls: List[str]
    scraped_at: datetime

class UtilityRate(BaseModel):
    """Retail electricity rate from a utility."""
    utility_name: str            # "Pacific Gas & Electric"
    utility_id: Optional[str]    # EIA utility ID
    state: str
    rate_schedule: str           # "E-TOU-C", "R-1", "SC-9 Rate II"
    customer_class: str          # "residential", "commercial", "industrial"
    rate_type: str               # "flat", "tiered", "tou", "demand"
    energy_rate_kwh: float       # $/kWh average
    peak_rate_kwh: Optional[float]
    off_peak_rate_kwh: Optional[float]
    demand_charge_kw: Optional[float]  # $/kW
    fixed_charge_monthly: Optional[float]
    net_metering: bool
    net_metering_type: Optional[str]   # "full_retail", "avoided_cost", "net_billing"
    effective_date: str
    source_url: str
    scraped_at: datetime

Step 2: Scrape Wholesale Electricity Prices

from mantis import MantisClient
import asyncio

mantis = MantisClient(api_key="your-mantis-api-key")

async def scrape_ercot_prices() -> List[EnergyPrice]:
    """
    Scrape ERCOT real-time and day-ahead prices.
    ERCOT operates the Texas grid โ€” the only major US market not
    subject to FERC jurisdiction, with unique price dynamics.
    """
    # Real-time Settlement Point Prices (SPPs)
    rt_result = await mantis.scrape(
        url="https://www.ercot.com/content/cdr/html/real_time_spp",
        extract={
            "prices": [{
                "settlement_point": "string (e.g., HB_HOUSTON, HB_NORTH, HB_SOUTH, HB_WEST, HB_PAN)",
                "price_usd_mwh": "number",
                "interval_start": "string (datetime)",
                "interval_end": "string (datetime)"
            }],
            "timestamp": "string (last updated time)"
        }
    )
    
    prices = []
    for p in rt_result.get("prices", []):
        prices.append(EnergyPrice(
            market=MarketRegion.ERCOT,
            price_type=PriceType.REAL_TIME,
            node_or_zone=p["settlement_point"],
            price_usd_mwh=p["price_usd_mwh"],
            timestamp=p["interval_start"],
            interval_minutes=5,
            is_peak=is_peak_hour(p["interval_start"]),
            scraped_at=datetime.now()
        ))
    
    return prices

async def scrape_caiso_prices() -> List[EnergyPrice]:
    """
    Scrape CAISO (California ISO) day-ahead prices.
    CAISO manages 80% of California's grid and features
    the famous 'duck curve' from solar overgeneration.
    """
    result = await mantis.scrape(
        url="https://www.caiso.com/TodaysOutlook/Pages/prices.html",
        extract={
            "prices": [{
                "zone": "string (SP15, NP15, ZP26)",
                "hour": "number (0-23)",
                "day_ahead_lmp": "number ($/MWh)",
                "congestion": "number ($/MWh)",
                "loss": "number ($/MWh)",
                "energy": "number ($/MWh)"
            }],
            "trade_date": "string"
        }
    )
    
    prices = []
    for p in result.get("prices", []):
        prices.append(EnergyPrice(
            market=MarketRegion.CAISO,
            price_type=PriceType.DAY_AHEAD,
            node_or_zone=p["zone"],
            price_usd_mwh=p["day_ahead_lmp"],
            congestion_component=p.get("congestion"),
            loss_component=p.get("loss"),
            energy_component=p.get("energy"),
            timestamp=f"{result['trade_date']}T{p['hour']:02d}:00:00",
            interval_minutes=60,
            is_peak=7 <= p["hour"] <= 22,
            scraped_at=datetime.now()
        ))
    
    return prices

async def scrape_pjm_prices() -> List[EnergyPrice]:
    """
    Scrape PJM Interconnection prices โ€” the world's largest
    wholesale electricity market, serving 65M+ people across
    13 states and DC.
    """
    result = await mantis.scrape(
        url="https://dataminer2.pjm.com/feed/rt_hrl_lmps",
        extract={
            "prices": [{
                "pricing_node": "string",
                "lmp": "number ($/MWh)",
                "congestion_price": "number",
                "marginal_loss_price": "number",
                "system_energy_price": "number",
                "datetime_beginning_ept": "string"
            }]
        }
    )
    
    return [
        EnergyPrice(
            market=MarketRegion.PJM,
            price_type=PriceType.REAL_TIME,
            node_or_zone=p["pricing_node"],
            price_usd_mwh=p["lmp"],
            congestion_component=p.get("congestion_price"),
            loss_component=p.get("marginal_loss_price"),
            energy_component=p.get("system_energy_price"),
            timestamp=p["datetime_beginning_ept"],
            interval_minutes=60,
            is_peak=True,
            scraped_at=datetime.now()
        )
        for p in result.get("prices", [])
    ]

def is_peak_hour(timestamp_str: str) -> bool:
    """NERC peak hours: Monday-Saturday, 6 AM - 10 PM."""
    dt = datetime.fromisoformat(timestamp_str)
    return dt.weekday() < 6 and 6 <= dt.hour <= 21

Step 3: Track Grid Conditions & Generation Mix

async def scrape_ercot_grid_status() -> GridStatus:
    """
    Monitor ERCOT grid conditions in real-time.
    Critical for detecting grid stress events โ€” Texas has
    no interconnections to fall back on during emergencies.
    """
    result = await mantis.scrape(
        url="https://www.ercot.com/gridmktinfo/dashboards",
        extract={
            "current_load_mw": "number",
            "forecast_peak_mw": "number",
            "total_generation_mw": "number",
            "wind_generation_mw": "number",
            "solar_generation_mw": "number",
            "thermal_generation_mw": "number",
            "nuclear_generation_mw": "number",
            "net_interchange_mw": "number",
            "operating_reserves_mw": "number",
            "system_frequency_hz": "number",
            "wind_curtailment_mw": "number or null",
            "solar_curtailment_mw": "number or null",
            "operating_condition": "string (normal, watch, advisory, emergency, EEA1, EEA2, EEA3)",
            "last_updated": "string"
        }
    )
    
    total_gen = result.get("total_generation_mw", 0)
    wind_mw = result.get("wind_generation_mw", 0)
    solar_mw = result.get("solar_generation_mw", 0)
    
    return GridStatus(
        market=MarketRegion.ERCOT,
        system_load_mw=result["current_load_mw"],
        forecast_load_mw=result.get("forecast_peak_mw"),
        total_generation_mw=total_gen,
        generation_by_fuel={
            "wind": wind_mw,
            "solar": solar_mw,
            "thermal": result.get("thermal_generation_mw", 0),
            "nuclear": result.get("nuclear_generation_mw", 0)
        },
        wind_penetration_pct=round(wind_mw / total_gen * 100, 1) if total_gen else None,
        solar_penetration_pct=round(solar_mw / total_gen * 100, 1) if total_gen else None,
        renewable_curtailment_mw=(result.get("wind_curtailment_mw", 0) or 0) + (result.get("solar_curtailment_mw", 0) or 0),
        frequency_hz=result.get("system_frequency_hz"),
        net_interchange_mw=result.get("net_interchange_mw"),
        operating_reserves_mw=result.get("operating_reserves_mw"),
        alert_level=result.get("operating_condition", "normal"),
        timestamp=result["last_updated"],
        scraped_at=datetime.now()
    )

async def scrape_caiso_renewables() -> dict:
    """
    Track CAISO's renewable generation and the famous 'duck curve'.
    California regularly hits 100%+ renewable supply during midday,
    creating negative prices and massive evening ramps.
    """
    result = await mantis.scrape(
        url="https://www.caiso.com/TodaysOutlook/Pages/supply.html",
        extract={
            "current_renewables_mw": "number",
            "current_renewables_pct": "number",
            "solar_mw": "number",
            "wind_mw": "number",
            "geothermal_mw": "number",
            "biomass_mw": "number",
            "small_hydro_mw": "number",
            "batteries_charging_mw": "number or null",
            "batteries_discharging_mw": "number or null",
            "curtailment_mw": "number or null",
            "net_demand_mw": "number (demand minus solar/wind)",
            "peak_renewables_today_pct": "number",
            "co2_intensity_lbs_mwh": "number or null"
        }
    )
    
    return {
        "renewables_mw": result.get("current_renewables_mw"),
        "renewables_pct": result.get("current_renewables_pct"),
        "solar_mw": result.get("solar_mw"),
        "wind_mw": result.get("wind_mw"),
        "battery_net_mw": (result.get("batteries_discharging_mw", 0) or 0) - (result.get("batteries_charging_mw", 0) or 0),
        "curtailment_mw": result.get("curtailment_mw", 0),
        "net_demand_mw": result.get("net_demand_mw"),
        "peak_renewables_pct": result.get("peak_renewables_today_pct"),
        "co2_intensity": result.get("co2_intensity_lbs_mwh")
    }

Step 4: Monitor Regulatory Filings & Rate Cases

async def scrape_ferc_filings(
    docket_prefix: str = "ER",
    days_back: int = 7
) -> List[RegulatoryFiling]:
    """
    Scrape FERC eLibrary for recent regulatory filings.
    FERC dockets contain rate changes, interconnection requests,
    merger applications, and market rule changes worth billions.
    """
    result = await mantis.scrape(
        url=f"https://elibrary.ferc.gov/eLibrary/search?q=*&dateRange=last{days_back}days&docketPrefix={docket_prefix}",
        extract={
            "filings": [{
                "docket_number": "string (e.g., ER25-1234-000)",
                "title": "string",
                "filer": "string (company name)",
                "filing_type": "string",
                "filed_date": "string (MM/DD/YYYY)",
                "description": "string",
                "status": "string",
                "document_count": "number"
            }],
            "total_results": "number"
        }
    )
    
    filings = []
    for f in result.get("filings", []):
        filings.append(RegulatoryFiling(
            docket_number=f["docket_number"],
            filing_type=classify_filing_type(f.get("title", ""), f.get("filing_type", "")),
            title=f["title"],
            filer=f["filer"],
            agency="FERC",
            status=f.get("status", "pending"),
            filed_date=f["filed_date"],
            description=f.get("description", ""),
            affected_states=extract_states_from_filing(f),
            document_urls=[],
            scraped_at=datetime.now()
        ))
    
    return filings

async def scrape_utility_rates(state: str = "CA") -> List[UtilityRate]:
    """
    Scrape retail electricity rates from state PUC or utility websites.
    EIA's OpenData API provides utility rate data, but tariff details
    require scraping individual utility rate schedules.
    """
    # Use EIA data for baseline rates
    eia_result = await mantis.scrape(
        url=f"https://www.eia.gov/electricity/state/{state.lower()}/",
        extract={
            "utilities": [{
                "name": "string",
                "residential_rate_cents_kwh": "number",
                "commercial_rate_cents_kwh": "number",
                "industrial_rate_cents_kwh": "number",
                "customers": "number",
                "revenue_millions": "number"
            }],
            "state_average_residential": "number (cents/kWh)",
            "state_average_commercial": "number (cents/kWh)",
            "national_average_residential": "number (cents/kWh)"
        }
    )
    
    rates = []
    for u in eia_result.get("utilities", []):
        rates.append(UtilityRate(
            utility_name=u["name"],
            state=state,
            rate_schedule="Standard",
            customer_class="residential",
            rate_type="flat",
            energy_rate_kwh=u["residential_rate_cents_kwh"] / 100,
            net_metering=state in ["CA", "NY", "NJ", "MA", "CO", "MN"],
            effective_date="2026-01-01",
            source_url=f"https://www.eia.gov/electricity/state/{state.lower()}/",
            scraped_at=datetime.now()
        ))
    
    return rates

async def scrape_interconnection_queue(market: MarketRegion) -> List[dict]:
    """
    Monitor ISO interconnection queues โ€” the pipeline of new
    generation projects (solar, wind, storage) seeking grid connection.
    Queue backlogs now exceed 2,000 GW nationally, with average
    wait times of 3-5 years.
    """
    urls = {
        MarketRegion.PJM: "https://www.pjm.com/planning/new-service-requests",
        MarketRegion.CAISO: "https://rimspub.caiso.com/rimsui/logon.do",
        MarketRegion.ERCOT: "https://www.ercot.com/gridinfo/resource",
        MarketRegion.MISO: "https://www.misoenergy.org/planning/generator-interconnection/GI_Queue/"
    }
    
    result = await mantis.scrape(
        url=urls.get(market, urls[MarketRegion.PJM]),
        extract={
            "projects": [{
                "queue_id": "string",
                "project_name": "string",
                "fuel_type": "string (solar, wind, battery, gas, hybrid)",
                "capacity_mw": "number",
                "county": "string",
                "state": "string",
                "status": "string (active, withdrawn, completed, suspended)",
                "request_date": "string",
                "expected_in_service": "string or null",
                "transmission_owner": "string"
            }],
            "total_queue_mw": "number",
            "total_projects": "number"
        }
    )
    
    return result.get("projects", [])

def classify_filing_type(title: str, raw_type: str) -> str:
    title_lower = title.lower()
    if "interconnection" in title_lower:
        return "interconnection"
    elif "rate" in title_lower or "tariff" in title_lower:
        return "rate_case"
    elif "merger" in title_lower or "acquisition" in title_lower:
        return "merger"
    elif "complaint" in title_lower:
        return "complaint"
    return raw_type or "other"

def extract_states_from_filing(filing: dict) -> List[str]:
    return []  # would parse from description in production

Step 5: AI-Powered Energy Market Analysis

import json
from openai import OpenAI

openai = OpenAI()

async def analyze_energy_market(
    prices: List[EnergyPrice],
    grid_status: GridStatus,
    filings: List[RegulatoryFiling]
) -> dict:
    """
    Use GPT-4o to identify anomalies, risks, and opportunities
    across energy market data streams.
    """
    prompt = f"""Analyze the following energy market data and provide actionable intelligence:

## Current Wholesale Prices (Last 24 Hours)
{json.dumps([{
    "market": p.market.value,
    "zone": p.node_or_zone,
    "price": p.price_usd_mwh,
    "type": p.price_type.value,
    "time": str(p.timestamp)
} for p in prices[:50]], indent=2)}

## Grid Conditions
- Market: {grid_status.market.value}
- System Load: {grid_status.system_load_mw:,.0f} MW
- Total Generation: {grid_status.total_generation_mw:,.0f} MW
- Generation Mix: {json.dumps(grid_status.generation_by_fuel)}
- Wind Penetration: {grid_status.wind_penetration_pct}%
- Solar Penetration: {grid_status.solar_penetration_pct}%
- Renewable Curtailment: {grid_status.renewable_curtailment_mw} MW
- Alert Level: {grid_status.alert_level}
- Operating Reserves: {grid_status.operating_reserves_mw} MW

## Recent Regulatory Filings
{json.dumps([{
    "docket": f.docket_number,
    "title": f.title,
    "filer": f.filer,
    "type": f.filing_type,
    "date": f.filed_date
} for f in filings[:20]], indent=2)}

Provide:
1. PRICE ANOMALIES: Any unusual price spikes, negative prices, or basis spreads that indicate trading opportunities or system stress
2. GRID RISK ASSESSMENT: Current grid reliability concerns โ€” reserves adequacy, renewable intermittency risks, congestion patterns
3. REGULATORY IMPACT: Which recent filings could materially impact energy costs, generation capacity, or market rules
4. RENEWABLE TRENDS: Solar/wind generation vs. forecasts, curtailment patterns, duck curve severity
5. TRADING SIGNALS: Specific actionable insights for energy traders (spread trades, congestion plays, weather-driven positions)
6. 7-DAY OUTLOOK: Based on current conditions and weather patterns, what should energy market participants prepare for

Format as structured JSON with severity ratings (low/medium/high/critical) for each finding."""

    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are a senior energy market analyst with expertise in ISO/RTO markets, power trading, renewable integration, and energy regulation. Provide precise, actionable intelligence that a trading desk or utility strategy team can act on immediately."},
            {"role": "user", "content": prompt}
        ],
        response_format={"type": "json_object"},
        temperature=0.2
    )
    
    return json.loads(response.choices[0].message.content)

async def detect_price_anomalies(prices: List[EnergyPrice]) -> List[dict]:
    """
    Detect price anomalies without AI โ€” rule-based screening for
    events that require immediate attention.
    """
    anomalies = []
    
    for price in prices:
        # Extreme price spike (>$500/MWh indicates scarcity)
        if price.price_usd_mwh > 500:
            anomalies.append({
                "type": "PRICE_SPIKE",
                "severity": "critical" if price.price_usd_mwh > 2000 else "high",
                "market": price.market.value,
                "zone": price.node_or_zone,
                "price": price.price_usd_mwh,
                "message": f"โšก {price.market.value.upper()} price spike: ${price.price_usd_mwh:.2f}/MWh at {price.node_or_zone}"
            })
        
        # Negative prices (overgeneration, typically renewable-driven)
        if price.price_usd_mwh < 0:
            anomalies.append({
                "type": "NEGATIVE_PRICE",
                "severity": "medium",
                "market": price.market.value,
                "zone": price.node_or_zone,
                "price": price.price_usd_mwh,
                "message": f"๐Ÿ“‰ {price.market.value.upper()} negative price: ${price.price_usd_mwh:.2f}/MWh at {price.node_or_zone} (renewable overgeneration likely)"
            })
        
        # Congestion indicates transmission constraints
        if price.congestion_component and abs(price.congestion_component) > 50:
            anomalies.append({
                "type": "CONGESTION",
                "severity": "high",
                "market": price.market.value,
                "zone": price.node_or_zone,
                "congestion": price.congestion_component,
                "message": f"๐Ÿšง High congestion at {price.node_or_zone}: ${price.congestion_component:.2f}/MWh"
            })
    
    return anomalies

async def monitor_grid_stress(status: GridStatus) -> List[dict]:
    """
    Check grid conditions for reliability concerns.
    """
    alerts = []
    
    # Low reserves warning
    if status.operating_reserves_mw and status.system_load_mw:
        reserve_margin_pct = (status.operating_reserves_mw / status.system_load_mw) * 100
        if reserve_margin_pct < 5:
            alerts.append({
                "type": "LOW_RESERVES",
                "severity": "critical",
                "message": f"๐Ÿ”ด {status.market.value.upper()} reserves at {reserve_margin_pct:.1f}% โ€” grid stress imminent",
                "reserves_mw": status.operating_reserves_mw,
                "load_mw": status.system_load_mw
            })
        elif reserve_margin_pct < 10:
            alerts.append({
                "type": "RESERVES_WARNING",
                "severity": "high",
                "message": f"๐ŸŸก {status.market.value.upper()} reserves at {reserve_margin_pct:.1f}% โ€” monitoring closely",
                "reserves_mw": status.operating_reserves_mw,
                "load_mw": status.system_load_mw
            })
    
    # Emergency alert level
    if status.alert_level and status.alert_level.startswith("EEA"):
        alerts.append({
            "type": "GRID_EMERGENCY",
            "severity": "critical",
            "message": f"๐Ÿšจ {status.market.value.upper()} grid emergency: {status.alert_level} declared",
            "alert_level": status.alert_level
        })
    
    # High renewable curtailment (>1 GW indicates transmission constraints)
    if status.renewable_curtailment_mw and status.renewable_curtailment_mw > 1000:
        alerts.append({
            "type": "HIGH_CURTAILMENT",
            "severity": "medium",
            "message": f"โœ‚๏ธ {status.market.value.upper()} curtailing {status.renewable_curtailment_mw:,.0f} MW of renewables โ€” storage/transmission opportunities",
            "curtailment_mw": status.renewable_curtailment_mw
        })
    
    # Frequency deviation (normal: 59.95-60.05 Hz)
    if status.frequency_hz and (status.frequency_hz < 59.95 or status.frequency_hz > 60.05):
        alerts.append({
            "type": "FREQUENCY_DEVIATION",
            "severity": "high",
            "message": f"โš ๏ธ {status.market.value.upper()} frequency at {status.frequency_hz:.3f} Hz (target: 60.000)",
            "frequency_hz": status.frequency_hz
        })
    
    return alerts

Step 6: Automated Alert Delivery

import httpx

SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

async def send_energy_alert(alert: dict):
    """Route energy alerts to Slack with severity-based formatting."""
    severity_emoji = {
        "critical": "๐Ÿ”ด",
        "high": "๐ŸŸ ",
        "medium": "๐ŸŸก",
        "low": "๐ŸŸข"
    }
    
    emoji = severity_emoji.get(alert.get("severity", "low"), "โ„น๏ธ")
    
    payload = {
        "text": f"{emoji} *Energy Alert โ€” {alert['type']}*\n{alert['message']}",
        "unfurl_links": False
    }
    
    async with httpx.AsyncClient() as client:
        await client.post(SLACK_WEBHOOK, json=payload)

async def daily_energy_briefing(
    prices: List[EnergyPrice],
    grid: GridStatus,
    filings: List[RegulatoryFiling],
    analysis: dict
):
    """
    Send a daily energy market briefing to the trading/strategy team.
    """
    # Calculate daily price statistics
    avg_price = sum(p.price_usd_mwh for p in prices) / len(prices) if prices else 0
    max_price = max((p.price_usd_mwh for p in prices), default=0)
    min_price = min((p.price_usd_mwh for p in prices), default=0)
    negative_hours = sum(1 for p in prices if p.price_usd_mwh < 0)
    
    briefing = f"""๐Ÿ“Š *Daily Energy Market Briefing*

*Price Summary ({grid.market.value.upper()})*
โ€ข Average LMP: ${avg_price:.2f}/MWh
โ€ข Peak: ${max_price:.2f}/MWh | Trough: ${min_price:.2f}/MWh
โ€ข Negative price intervals: {negative_hours}

*Grid Conditions*
โ€ข Load: {grid.system_load_mw:,.0f} MW | Generation: {grid.total_generation_mw:,.0f} MW
โ€ข Wind: {grid.wind_penetration_pct:.1f}% | Solar: {grid.solar_penetration_pct:.1f}%
โ€ข Curtailment: {grid.renewable_curtailment_mw:,.0f} MW
โ€ข Status: {grid.alert_level}

*Regulatory Activity*
โ€ข New filings (7 days): {len(filings)}
โ€ข Rate cases: {sum(1 for f in filings if f.filing_type == 'rate_case')}
โ€ข Interconnection: {sum(1 for f in filings if f.filing_type == 'interconnection')}

*AI Analysis Highlights*
{chr(10).join('โ€ข ' + finding for finding in analysis.get('key_findings', ['No notable findings'])[:5])}
"""
    
    async with httpx.AsyncClient() as client:
        await client.post(SLACK_WEBHOOK, json={"text": briefing})

Advanced: Multi-Market Arbitrage Detection Engine

async def detect_arbitrage_opportunities(
    all_prices: dict  # {MarketRegion: List[EnergyPrice]}
) -> List[dict]:
    """
    Identify cross-market and cross-time arbitrage opportunities.
    
    Energy traders profit from:
    1. Spatial arbitrage: buy cheap power in one zone, sell in congested zone
    2. Temporal arbitrage: buy during negative-price solar hours, sell during evening ramp
    3. Basis trades: trade the spread between gas hubs or electricity zones
    4. Virtual trading: financial positions on day-ahead vs real-time price differences
    """
    opportunities = []
    
    for market, prices in all_prices.items():
        # Find peak-offpeak spread (temporal arbitrage)
        peak_prices = [p for p in prices if p.is_peak]
        offpeak_prices = [p for p in prices if not p.is_peak]
        
        if peak_prices and offpeak_prices:
            avg_peak = sum(p.price_usd_mwh for p in peak_prices) / len(peak_prices)
            avg_offpeak = sum(p.price_usd_mwh for p in offpeak_prices) / len(offpeak_prices)
            spread = avg_peak - avg_offpeak
            
            if spread > 30:  # $30/MWh spread is significant
                opportunities.append({
                    "type": "PEAK_OFFPEAK_SPREAD",
                    "market": market.value,
                    "spread_usd_mwh": round(spread, 2),
                    "avg_peak": round(avg_peak, 2),
                    "avg_offpeak": round(avg_offpeak, 2),
                    "signal": "Buy/charge during off-peak, sell/discharge during peak",
                    "ideal_for": "Battery storage operators, pumped hydro"
                })
        
        # Find zonal spread (spatial arbitrage)
        by_zone = {}
        for p in prices:
            by_zone.setdefault(p.node_or_zone, []).append(p.price_usd_mwh)
        
        zone_avgs = {zone: sum(vals)/len(vals) for zone, vals in by_zone.items()}
        
        if len(zone_avgs) >= 2:
            cheapest = min(zone_avgs, key=zone_avgs.get)
            expensive = max(zone_avgs, key=zone_avgs.get)
            zonal_spread = zone_avgs[expensive] - zone_avgs[cheapest]
            
            if zonal_spread > 20:
                opportunities.append({
                    "type": "ZONAL_SPREAD",
                    "market": market.value,
                    "cheap_zone": cheapest,
                    "expensive_zone": expensive,
                    "spread_usd_mwh": round(zonal_spread, 2),
                    "signal": f"Congestion play: {cheapest} โ†’ {expensive}",
                    "indicates": "Transmission constraints โ€” potential FTR value"
                })
        
        # Detect DA-RT spread opportunity (virtual trading)
        da_prices = {p.node_or_zone: p.price_usd_mwh for p in prices if p.price_type == PriceType.DAY_AHEAD}
        rt_prices = {}
        for p in prices:
            if p.price_type == PriceType.REAL_TIME:
                rt_prices.setdefault(p.node_or_zone, []).append(p.price_usd_mwh)
        
        for zone in da_prices:
            if zone in rt_prices:
                avg_rt = sum(rt_prices[zone]) / len(rt_prices[zone])
                da_rt_spread = da_prices[zone] - avg_rt
                
                if abs(da_rt_spread) > 15:
                    opportunities.append({
                        "type": "DA_RT_SPREAD",
                        "market": market.value,
                        "zone": zone,
                        "da_price": round(da_prices[zone], 2),
                        "avg_rt_price": round(avg_rt, 2),
                        "spread": round(da_rt_spread, 2),
                        "signal": "Virtual bid" if da_rt_spread > 0 else "Virtual offer",
                        "confidence": "Requires weather and load forecast validation"
                    })
    
    return sorted(opportunities, key=lambda x: abs(x.get("spread_usd_mwh", 0)), reverse=True)

Cost Comparison: Enterprise Energy Analytics vs. AI Agents

PlatformMonthly CostKey Features
S&P Global Platts$3,000โ€“$30,000Price assessments, benchmarks, analytics, news, global commodity coverage
Wood Mackenzie / Genscape$5,000โ€“$50,000Real-time power plant monitoring, fundamental analytics, weather-adjusted forecasts
ICF International$10,000โ€“$100,000Consulting + analytics, integrated planning models, regulatory support
Yes Energy / PE$3,000โ€“$25,000ISO/RTO data aggregation, LMP analytics, congestion analysis, FTR tools
Hitachi Energy (ABB)$5,000โ€“$30,000Grid analytics, SCADA integration, asset performance management
Aurora Energy Research$5,000โ€“$40,000Long-term power market forecasts, renewable valuation, policy analysis
AI Agent + Mantis$29โ€“$299Wholesale prices, grid monitoring, regulatory filings, utility rates โ€” fully custom

Honest caveat: Enterprise energy platforms like S&P Global Platts and Wood Mackenzie have proprietary datasets that no web scraping can replicate โ€” decades of assessed commodity prices, real-time generation plant monitoring via sensors (Genscape's satellite and infrared monitoring), proprietary weather-adjusted load forecasts trained on years of grid data, and deep relationships with market participants for non-public transaction data. Yes Energy aggregates ISO data into analytics-ready databases with historical archives going back 20+ years. The AI agent approach excels at real-time price monitoring across markets, regulatory filing tracking, utility rate comparison, and custom market intelligence โ€” the fast-moving data layer where you need alerts in minutes, not the deep fundamental analysis where enterprise platforms invest billions in proprietary data collection. For independent power producers, energy consultants, renewable developers, and trading shops that don't need $50K/month platforms, an AI agent covers 70โ€“80% of market intelligence needs at 1โ€“5% of the cost.

Use Cases by Energy Segment

1. Energy Traders & Hedge Funds

Monitor real-time LMPs across all seven US ISO/RTO markets simultaneously. Detect price spikes, congestion events, and negative price intervals within seconds. Track day-ahead vs. real-time spreads for virtual trading opportunities. Monitor natural gas basis differentials between Henry Hub, Waha, SoCal Citygate, and Algonquin. Build automated alerts for price anomalies that indicate grid stress or renewable overgeneration โ€” events where informed traders profit while others react.

2. Utilities & Grid Operators

Track peer utility rate cases to anticipate regulatory trends and benchmark your own rate proposals. Monitor interconnection queues to understand new generation coming online in your service territory. Scrape state PUC dockets for early notice of regulatory proceedings that affect your operations. Track renewable curtailment patterns to inform transmission investment decisions. Monitor customer review sentiment on social media during outages for PR response.

3. Renewable Energy Developers

Monitor PPA prices and REC values across states to optimize project siting and offtake negotiations. Track interconnection queue positions and timelines across ISOs. Scrape utility IRP (Integrated Resource Plan) filings to identify upcoming procurement opportunities. Monitor curtailment data to avoid developing in congested areas. Track IRA incentive guidance from IRS and DOE for tax credit optimization โ€” the difference between a $3/MWh and $27/MWh PTC matters.

4. Energy Consultants & Regulators

Build comprehensive market monitoring dashboards for clients across multiple markets. Track regulatory proceedings across FERC and all 50 state PUCs simultaneously. Monitor wholesale and retail rate trends to advise on procurement strategy. Aggregate grid performance data to support reliability studies. Track carbon market prices (RGGI, WCI, voluntary markets) for emissions compliance and ESG reporting.

Compliance & Best Practices

Getting Started

  1. Pick your market โ€” Start with one ISO/RTO (ERCOT and CAISO have the most accessible public dashboards)
  2. Set up Mantis API access โ€” sign up for a free API key (100 calls/month free)
  3. Start with price monitoring โ€” Track real-time LMPs and day-ahead prices at key hubs; you'll immediately see patterns (solar duck curves, evening ramps, weather-driven spikes)
  4. Add grid status โ€” Monitor generation mix, reserves, and alert levels; correlate with price movements
  5. Layer in regulatory tracking โ€” Set up weekly FERC filing scans filtered to your market; rate cases and interconnection filings are the most impactful
  6. Automate alerts โ€” Route price spikes >$200/MWh, negative prices, grid emergencies, and new rate case filings to your team via Slack