Skip to content

About

Data layer for Kardashev Labs. Ingests US grid datasets from ISO/RTO and EIA sources into Postgres and serves them through a read-only FastAPI API.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

kardashev-data

Self-hosted US energy grid data platform. Ingests real-time and historical datasets from ISO/EIA/ERCOT/CAISO and other public sources into Postgres, serves them through a read-only REST API, and powers the Kardashev Labs tools.

Live API: https://data.kardashevlabs.org (interactive docs)


Quick start

No authentication required.

# Real-time carbon intensity (lbs CO2/MWh) for all US ISOs
curl https://data.kardashevlabs.org/carbon/latest

# Live LMP prices at NYISO zone hubs
curl "https://data.kardashevlabs.org/lmp?iso=NYISO&market=RT&limit=20"

# Nodal price map: every priced node in ERCOT right now
curl "https://data.kardashevlabs.org/lmp/map?iso=ERCOT&market=RT"

# Fuel mix across all 7 ISOs (last 24h)
curl "https://data.kardashevlabs.org/fuel-mix?iso=CAISO"

# Solar + wind curtailment in California (last 30 days)
curl "https://data.kardashevlabs.org/curtailment?iso=CAISO&days=30"

# Henry Hub nat gas price history
curl "https://data.kardashevlabs.org/natural-gas/latest"

Datasets

Endpoint Cadence ISOs / Sources What it is
/fuel-mix 5 min All 7 ISOs + EIA-930 BAs Generation by fuel type (MW)
/carbon/latest 5 min All 7 ISOs + EIA-930 BAs CO₂ intensity (lbs/MWh), eGRID factors
/lmp 5 min (RT), hourly (DA) NYISO, ERCOT, MISO, SPP, CAISO, PJM, ISONE Locational marginal prices: energy, congestion, loss
/lmp/map 5 min NYISO, ERCOT, MISO, SPP, CAISO, PJM, ISONE Nodal prices with lat/lng for map rendering
/curtailment Daily CAISO, ERCOT, SPP Solar + wind curtailment (MWh/day)
/load 5 min (RT), hourly (EIA) CAISO, NYISO, ISONE + EIA-930 BAs Real-time grid load (MW)
/load-forecast Hourly PJM, ISONE, NYISO, MISO, SPP, ERCOT Load forecast track record + recent forecasts
/generation/wind-solar 5 min ERCOT, PJM Wind + solar actual vs. forecast
/generation/battery 5 min CAISO Battery storage charge/discharge (MW)
/generation/btm-solar Hourly (currently disabled) NYISO Behind-the-meter solar actual vs. forecast — disabled since the upstream NYISO endpoint was discontinued (June 2026)
/generation/reserve-margins Hourly PJM Capacity reserve margin forecast
/natural-gas Daily EIA Henry Hub + regional hub spot prices
/natural-gas/storage Daily EIA Natural gas in storage (Bcf)
/nuclear Daily NRC US reactor status: % capacity, rolling 365 days
/emissions Daily EPA CAMPD Hourly SO₂, NOₓ, CO₂ by plant
/carbon-markets Weekly poll RGGI, CA ARB Carbon allowance auction prices ($/ton)
/commodities/coal Weekly poll EIA Coal spot prices by region
/commodities/petroleum Weekly poll EIA WTI, RBOB, diesel, jet fuel
/commodities/power-burn Weekly poll EIA Natural gas power burn (Bcf/d)
/commodities/forecasts/steo Weekly poll EIA STEO Short-Term Energy Outlook forecast series
/hydro/reservoirs Daily USBR Western reservoir storage (% capacity)
/hydro/streamflow Daily USGS River gauge readings (cfs)
/solar/irradiance Daily NREL NSRDB GHI/DNI/DHI at 10 US grid locations
/interchange Hourly EIA Balancing authority electricity flows
/interconnection-queue Daily NYISO, PJM, ISONE Interconnection queue (project, fuel, MW, status)
/ercot/large-load ~Monthly ERCOT Large-load interconnection queue, vision-extracted from ERCOT's chart-only PDF
/forecast Daily/hourly (internal) DA/RT spread forecast + track record for the forecasting model
/outages Daily CAISO, MISO Generator outages
/ancillary 5 min CAISO, ERCOT Ancillary services prices
/isos On request All 7 ISOs Metadata: ISO status/coverage summary
/weather Hourly NOAA Grid-node temperatures (°F)
/constraints 5 min MISO Binding transmission constraints
/bpa 5 min BPA Bonneville Power Authority load + generation
/eia/capacity Weekly EIA-860 US generator nameplate capacity by fuel
/eia/generation Weekly EIA-923 Monthly generation by fuel (MWh)
/eia/retail-prices Weekly EIA-861 State retail electricity prices (¢/kWh)

LMP data also has a standalone historical backfill path (ingest/backfill_lmp.py, not part of the live scheduler) that pulls ERCOT MIS yearly DAM/RTM hub + load-zone price files and CAISO OASIS PRC_LMP/PRC_INTVL_LMP to build out multi-year history for the forecasting dataset. LMP retention in the live table defaults to 10 years (LMP_RETENTION_DAYS).


Architecture

ISO APIs / EIA / ERCOT CDR / CAISO OASIS / NYISO Open Data
        ↓ (Python ingest workers)
   PostgreSQL (time-series schema)
        ↓ (FastAPI, asyncpg)
   REST API  →  kardashevlabs.org tools

Stack:

  • Python 3.12, FastAPI, asyncpg
  • PostgreSQL (Railway managed)
  • Ingest scheduler: ingest/scheduler.py, a simple while True loop with per-job cadence tracking (no external scheduler library)
  • kardashev package (separate PyPI package, pinned in pyproject.toml) for direct ISO data access (CAISO, ERCOT, MISO, NYISO, ISONE, SPP, PJM); iso_data/ in this repo retains only supplementary, non-ISO sources (BPA, NOAA weather, NRC, EPA CAMPD, RGGI, USBR, NREL, EIA-930/commodities)
  • ingest/backfill_lmp.py: standalone CLI for historical LMP backfill (ERCOT MIS yearly files, CAISO OASIS), run manually/out-of-band rather than on the live scheduler
  • Railway (API service + ingest worker, two services sharing one Dockerfile, both auto-deploy from main)

Data sources: CAISO OASIS, ERCOT CDR/MIS, NYISO Open Data, MISO API, SPP Marketplace, PJM Data Miner, ISO-NE web services, EIA Open Data API, NRC daily status, EPA CAMPD API, USBR HydroMet, USGS Water Services, NREL NSRDB, NOAA weather, RGGI/CA ARB auction results.


ERCOT GIS filing history and corrections

GIS ingestion preserves each source document and extraction in ercot_gis_filings and its project rows in ercot_gis_observations. Filings include ERCOT's document ID, download URL, filename, publication timestamp, SHA-256, exact downloaded bytes (raw_file), parser version, and extraction timestamp. filing_id joins the source file to every extracted row; project identity remains ERCOT's INR (queue_id).

The scheduler checks daily at 16:00 UTC. Each available file is downloaded and hashed, including previously seen document IDs, so changed bytes under the same ID are detectable. Identical document ID/publication/hash/parser combinations are skipped. This intentionally uses more bandwidth than checking only filenames. Increment PARSER_VERSION in ingest/ercot_gis.py when correcting extraction logic; the next run stores a new extraction while retaining the earlier one. A failed or empty parse leaves existing history intact and is retried on a later run.

ercot_gis_snapshots remains the compatible monthly projection for existing readers. It selects the latest publication for each month; extraction time and filing ID break publication-time ties. Replacement is atomic and removes projects omitted by a corrected filing. Older backfills cannot supersede later publications. Pre-migration rows are retained separately in ercot_gis_legacy_snapshots: they have no verified source-document provenance and are not represented as new auditable filings. A migration alone cannot recover previously missed revisions.

Rollout (in the deployed environment with its configured database):

python -m db.migrate
python -m ingest.ercot_gis --backfill
python -m ingest.ercot_gis_timelines

The backfill checks all files still available from ERCOT; it cannot recover files ERCOT has removed or overwritten before they were archived. Both normal and --backfill runs now inspect all available documents. --dry-run --limit 1 checks extraction without accessing the database.

Inspect a project's source-backed history:

SELECT f.filing_id, f.source_document_id, f.source_url, f.published_at,
       f.extracted_at, f.content_sha256, f.parser_version,
       o.snapshot_month, o.queue_id, o.capacity_mw, o.projected_cod
FROM ercot_gis_observations o
JOIN ercot_gis_filings f USING (filing_id)
WHERE o.queue_id = 'YOUR_ERCOT_INR'
ORDER BY f.published_at, f.extracted_at, f.filing_id;

Retrieve the exact source bytes with SELECT raw_file FROM ercot_gis_filings WHERE filing_id = .... Publication time describes ERCOT's metadata; extraction time describes when this system captured that version. For a historical "what had Kardashev captured by then" query, constrain both timestamps. The monthly projection and existing aggregate metrics are not an as-of audit API.

Storage regression tests use isolated schemas in a disposable PostgreSQL database:

GIS_TEST_DATABASE_URL=postgresql://localhost/kardashev_test pytest tests/test_gis_filing_storage.py -q

Tools built on this API

Tool URL What it shows
Nodal LMP Price Map lmp-map.kardashevlabs.org Live nodal electricity prices on a map, all ISOs, 60s refresh
Carbon Intensity Dashboard carbon-dashboard.kardashevlabs.org Real-time CO₂ intensity, 27+ US grid regions
LMP Dashboard lmp.kardashevlabs.org Electricity spot prices + fuel mix + context
Curtailment Tracker curtailment-tracker.kardashevlabs.org Daily renewable curtailment by ISO
Interconnection Queue interconnection-queue.kardashevlabs.org Every US power project waiting to connect to the grid
Grid Demand grid-demand.kardashevlabs.org Real-time load across 15 balancing authorities

Running locally

python -m venv .venv && source .venv/bin/activate
pip install -e ".[api,ingest,dev]"
cp .env.example .env          # fill in DATABASE_URL and EIA_API_KEY

Docker (API + ingest + Postgres all-in-one):

docker compose up --build

Or against a local Postgres:

python -m db.migrate           # create schema
uvicorn api.main:app --reload  # API at localhost:8000
python -m ingest.scheduler     # start data collection

Interactive docs at http://localhost:8000/docs.

Historical LMP backfill (separate from the live scheduler):

python -m ingest.backfill_lmp ercot --start-year 2019
python -m ingest.backfill_lmp caiso --start 2024-01-01 --end 2026-07-06

Environment variables

Variable Required Purpose
DATABASE_URL yes Postgres connection string
EIA_API_KEY most EIA-sourced jobs Free at eia.gov/opendata
PJM_API_KEY no Optional override for api.pjm.com; default is the public DataMiner2 subscriptionKey from settings.json (used for 5-min inst_load + RT LMP)
PJM_USERNAME / PJM_PASSWORD legacy only Old DataMiner2 CSV feeds — dead as of 2026-07
ISONE_USERNAME / ISONE_PASSWORD ISONE LMP/load/queue jobs Free at iso-ne.com
ANTHROPIC_API_KEY ERCOT large-load ingest only Vision extraction of ERCOT's chart-only large-load PDF
ANOMALY_ENABLED no Default 1. Set 0 to disable the anomaly watcher on the ingest service
ANOMALY_NOTIFY_OPS no Default off. Set 1 to also Slack/email iso_silent ingest-ops alerts
ANOMALY_NOTIFY_EMAIL no Inbox for anomaly emails (e.g. your Gmail)
ANOMALY_EMAIL_WEBHOOK_URL with email on Railway HTTPS Google Apps Script web app URL (ingest/anomaly_gmail_bridge.gs) — Railway blocks SMTP to Gmail
ANOMALY_EMAIL_WEBHOOK_TOKEN no Shared secret; must match Script property WEBHOOK_TOKEN
ANOMALY_SMTP_HOST / USER / PASSWORD optional Local/dev SMTP only; smtp.gmail.com times out from Railway
ANOMALY_SMTP_PORT no Default 587 (STARTTLS)
ANOMALY_EMAIL_FROM no SMTP From header; defaults to ANOMALY_SMTP_USER
ANOMALY_WEBHOOK_URL / SLACK_WEBHOOK_URL no Optional Slack-compatible webhook in addition to email
CORS_ORIGINS no Comma-separated extra CORS origins (*.kardashevlabs.org is always allowed)
ENVIRONMENT no Present in .env.example; not currently read by the app (/docs and /redoc are always on)
RATE_LIMIT_RPM no API rate limit per IP, requests per minute (default in .env.example is 120)

.env.example in this repo covers the API-service variables above; ISO keys and anomaly webhook/email vars belong on the ingest service.


Tests

pytest -q

License

MIT

About

Data layer for Kardashev Labs. Ingests US grid datasets from ISO/RTO and EIA sources into Postgres and serves them through a read-only FastAPI API.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages