Web Scraping for Energy & Utilities: How AI Agents Track Prices, Grid Data, Regulations & Market Intelligence in 2026
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:
- Wholesale price monitoring โ Track real-time and day-ahead LMPs across ISO/RTO markets (ERCOT, PJM, CAISO, NYISO, MISO, SPP, ISO-NE) at nodal and zonal levels
- Grid condition tracking โ Monitor grid frequency, load forecasts, generation mix, congestion, and emergency alerts from system operators
- Regulatory intelligence โ Track FERC filings, state PUC dockets, interconnection queues, rate cases, and environmental compliance deadlines
- Utility rate analysis โ Monitor retail electricity and gas rates across 3,000+ US utilities, including time-of-use rates, demand charges, and net metering policies
- Renewable energy markets โ Track PPA prices, REC values, solar/wind auction results, LCOE trends, and IRA incentive changes
- Natural gas & commodities โ Monitor Henry Hub, regional basis differentials, storage levels, LNG export terminal flows, and pipeline capacity
- Carbon & emissions markets โ Track RGGI, WCI, and EU ETS carbon allowance prices, offset registries, and compliance deadlines
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
- Wholesale price scraping โ Monitor real-time and day-ahead LMPs, natural gas spot prices, and commodity benchmarks across markets
- Grid & generation tracking โ Track system load, generation mix (solar/wind/gas/nuclear), frequency, and congestion patterns
- Regulatory filing monitoring โ Scrape FERC eLibrary, state PUC dockets, and interconnection queues for new filings
- Utility rate intelligence โ Monitor retail rate changes, TOU schedules, demand charges, and net metering policies across utilities
- GPT-4o market analysis โ Identify price anomalies, regulatory risks, arbitrage opportunities, and renewable energy trends
- 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
| Platform | Monthly Cost | Key Features |
|---|---|---|
| S&P Global Platts | $3,000โ$30,000 | Price assessments, benchmarks, analytics, news, global commodity coverage |
| Wood Mackenzie / Genscape | $5,000โ$50,000 | Real-time power plant monitoring, fundamental analytics, weather-adjusted forecasts |
| ICF International | $10,000โ$100,000 | Consulting + analytics, integrated planning models, regulatory support |
| Yes Energy / PE | $3,000โ$25,000 | ISO/RTO data aggregation, LMP analytics, congestion analysis, FTR tools |
| Hitachi Energy (ABB) | $5,000โ$30,000 | Grid analytics, SCADA integration, asset performance management |
| Aurora Energy Research | $5,000โ$40,000 | Long-term power market forecasts, renewable valuation, policy analysis |
| AI Agent + Mantis | $29โ$299 | Wholesale 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
- ISO/RTO data is public โ wholesale electricity prices, grid conditions, and market reports are published by ISOs as part of their FERC-mandated transparency requirements
- FERC filings are public records โ the eLibrary is a public document repository; all filings and orders are available to anyone
- EIA data is public โ US Energy Information Administration statistics are taxpayer-funded public data
- Utility rates are public โ tariff schedules filed with PUCs are public documents; utilities publish rates on their websites
- NERC CIP compliance โ never attempt to access SCADA systems, control systems, or any critical energy infrastructure (CIP-protected systems); only scrape public-facing websites
- Rate limiting โ ISO websites serve critical grid operations; respect rate limits and cache aggressively; price data updates every 5 minutes, not every second
- Market manipulation โ using scraped data for trading is legal; manipulating markets using false data or wash trading is a FERC violation under Anti-Manipulation Rule (18 CFR 1c)
Getting Started
- Pick your market โ Start with one ISO/RTO (ERCOT and CAISO have the most accessible public dashboards)
- Set up Mantis API access โ sign up for a free API key (100 calls/month free)
- 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)
- Add grid status โ Monitor generation mix, reserves, and alert levels; correlate with price movements
- Layer in regulatory tracking โ Set up weekly FERC filing scans filtered to your market; rate cases and interconnection filings are the most impactful
- Automate alerts โ Route price spikes >$200/MWh, negative prices, grid emergencies, and new rate case filings to your team via Slack