Skip to Content
ConceptsThe EIA data model

The EIA data model

Claros turns U.S. Energy Information Administration (EIA) data into verifiable on-chain readings. To do that well, you need a precise mental model of how EIA exposes its data, because that model is exactly what a Claros feed_id encodes.

The good news: EIA APIv2 is one uniform, faceted API. Electricity, natural gas, petroleum, coal, nuclear, SEDS, CO2, outlooks, and international data are all reached the same way. Claros ships a single generic adapter on top of it (agent/src/eia.ts), so adding any new series is one row of configuration, never a new client.

Every EIA dataset is read through one endpoint shape:

GET https://api.eia.gov/v2/{route}/data ?api_key={key} &frequency={frequency} &data[0]={column} &facets[{facetId}][]={value} (repeat once per facet) &sort[0][column]=period &sort[0][direction]=desc &offset=0 &length=1

Claros always asks for the single most recent row (length=1, sorted by period descending). The four placeholders route, frequency, column, and facets are the only things that change between datasets. Those are the four knobs.

The four knobs

Any EIA series, and therefore any Claros feed, is fully determined by four selectors. Pick all four and you have pinned exactly one stream of numbers.

KnobQuery parameterWhat it selectsExample
Routepath /{route}/which dataset (energy family + table)petroleum/pri/spt
Frequencyfrequency=the time granularity (pick exactly one)daily
Columndata[0]=which numeric column you readvalue
Facetsfacets[id][]=the filters that narrow to one row per periodseries=RWTC

In code, a Claros feed is precisely this tuple plus two presentation fields. From the EiaFeed interface in agent/src/eia.ts:

export interface EiaFeed { asset_id: string; // the Claros feed_id, e.g. "EIA.PET.PRICE.WTI.DAILY" route: string; // "petroleum/pri/spt" frequency: string; // "daily" | "weekly" | "monthly" | "annual" | "hourly" | "quarterly" data_col: string; // "value" | "price" | "quantity" | ... facets: Record<string, string>; // { series: "RWTC" } (the filters) unit: string; // "$/bbl" (metadata, how to label it) decimals: number; // 6 (metadata, how to scale it on-chain) }

The selection tuple is { route, frequency, data_col, facets }. The unit and decimals fields do not select the number, they describe how to interpret it: unit is the human label and decimals is the fixed-point scale recorded as feed metadata. (See reading values for how amount and decimals combine into a human value.)

Route

The route is the dataset path under /v2/. It reads as a family followed by a table, for example petroleum/pri/spt is petroleum, then prices, then spot. The route alone fixes which physical table you are querying and, with it, the set of frequencies, columns, and facets that table supports.

Claros tracks 232 EIA datasets (routes) in its catalog and currently maps 37 of them to live feeds. Some flagship routes:

RouteFamilyReads
petroleum/pri/sptPetroleumcrude oil spot prices (WTI, Brent)
natural-gas/pri/futNatural gasHenry Hub spot and futures
electricity/retail-salesElectricityretail price, sales, revenue, customers
electricity/rto/region-dataElectricityhourly grid demand by region
coal/price-by-rankCoalcoal price by rank
co2-emissions/co2-emissions-aggregatesEmissionsCO2 by state, fuel, sector
internationalInternationalglobal production and consumption

Frequency

A route publishes the same underlying data at several cadences. You choose exactly one with frequency. The allowed values across EIA are daily, weekly, monthly, quarterly, annual, and hourly. The route’s catalog entry lists which of these it supports and a defaultFrequency.

Each frequency also fixes the shape of the period string EIA returns:

FrequencyEIA period formatExample
hourlyYYYY-MM-DD"T"HH2026-06-19T14
dailyYYYY-MM-DD2026-06-19
weeklyYYYY-MM-DD2026-06-19
monthlyYYYY-MM2026-06
quarterlyYYYY-"Q"Q2026-Q2
annualYYYY2026

On-chain, the period is stored as an integer. The adapter (periodToNumber in agent/src/eia.ts) strips the separators, so 2026-06-19 becomes 20260619 and 2026-06 becomes 202606. Hourly periods are stored as epoch seconds instead. Picking the frequency therefore also decides what the on-chain period looks like.

Column

A single EIA row can carry several numeric columns. The data[0] parameter chooses the one you actually read, and Claros reads exactly one column per feed. Common columns:

Column (data[0])MeaningExample feed
valuethe dataset’s primary valueEIA.PET.PRICE.WTI.DAILY
pricea price columnEIA.ELEC.RETAIL.PRICE.US_ALL.MONTHLY
salesa sales / volume columnEIA.ELEC.RETAIL.SALES.US_ALL.MONTHLY
revenuea revenue columnEIA.ELEC.RETAIL.REVENUE.US_ALL.MONTHLY
customersa count columnEIA.ELEC.RETAIL.CUSTOMERS.US_RES.MONTHLY
quantitya quantity columnEIA.PET.IMPORTS.CRUDE_SAU.MONTHLY
productiona production columnEIA.COAL.PROD.US.ANNUAL
percentOutagepercent of capacity offlineEIA.NUC.OUTAGE.US_PCT.DAILY
capacitya capacity columnEIA.NUC.OUTAGE.US_CAPACITY.DAILY

The column matters as much as the filter. The four EIA.ELEC.RETAIL.* feeds above share the same route, frequency, and facets and differ only in this column, yet they return four completely different numbers (a price, a volume, a dollar amount, and a head count).

Facets (the filters)

Facets are the filters people mean when they ask “which filter do I use.” A route defines a fixed set of facets, each identified by an id (such as series, duoarea, process, stateid, fueltype). To select a row you choose a value for the facets that matter, sent as facets[{id}][]={value}.

These are the facet ids used by the live Claros feeds, with the description straight from the catalog and the value the feed pins:

Facet idCatalog descriptionExample valueUsed by feed
seriesSeriesRWTCEIA.PET.PRICE.WTI.DAILY
duoareaDuoAreaNUSEIA.NG.PROD.MARKETED.MONTHLY
processProcessVGMEIA.NG.PROD.MARKETED.MONTHLY
stateidState / Census RegionUSEIA.ELEC.RETAIL.PRICE.US_ALL.MONTHLY
sectoridSectorALLEIA.ELEC.RETAIL.PRICE.US_ALL.MONTHLY
respondentBalancing Authority / RegionUS48EIA.ELEC.DEMAND.US48.HOURLY
typeMetricDEIA.ELEC.DEMAND.US48.HOURLY
fueltypeEnergy SourceNGEIA.ELEC.GEN_NG.US48.HOURLY
stateRegionIdState / RegionUSEIA.COAL.PROD.US.ANNUAL
coalRankIdCoal RankBITEIA.COAL.PRICE.BITUMINOUS.ANNUAL
msnUnique series identifierTETCBUSEIA.TOTAL.PRIMARY_CONS.MONTHLY
seriesIdUnique series identifierWTIPUUSEIA.STEO.WTI_PRICE.MONTHLY
stateIdStateCAEIA.SEDS.TOTAL_CONS.CA.ANNUAL
originIdOrigin IdCTY_SAEIA.PET.IMPORTS.CRUDE_SAU.MONTHLY

Facet ids are dataset-specific and are easy to confuse. series (petroleum, natural gas) is not the same as seriesId (STEO, SEDS); stateid (electricity) is not stateId (SEDS, CO2) is not stateRegionId (coal). Always read the facet ids from the route’s own catalog entry rather than reusing one from another family.

Two facet patterns are worth calling out:

  • Empty facets. Some routes already resolve to a single national series, so the feed sets facets: {} and just takes the latest row. The nuclear-outage and densified-biomass feeds do this, for example EIA.NUC.OUTAGE.US_PCT.DAILY.
  • A facet can pin the unit. A few routes expose unit as a facet. The international crude-production feed uses facets: { activityId: "1", productId: "57", countryRegionId: "WORL", unit: "TBPD" }, where unit=TBPD (thousand barrels per day) is itself a filter, not just a label.

Worked example: EIA.PET.PRICE.WTI.DAILY

This is the WTI crude oil spot price. Its definition in agent/src/eia-feeds.ts is one row:

{ asset_id: 'EIA.PET.PRICE.WTI.DAILY', route: 'petroleum/pri/spt', frequency: 'daily', data_col: 'value', facets: { series: 'RWTC' }, unit: '$/bbl', decimals: 6, }

Here is how each knob maps onto the EIA request.

Route: petroleum/pri/spt

Petroleum spot prices. This route supports daily, weekly, monthly, and annual, exposes a single column value, and offers the facets duoarea, product, process, and series.

Frequency: daily

We want the daily quote, so frequency=daily. Periods will arrive as YYYY-MM-DD and be stored on-chain as YYYYMMDD.

Column: value

This route’s numeric column is value, so data[0]=value.

Facet: series=RWTC

The route carries many price series. RWTC is EIA’s code for “Cushing, OK WTI spot price.” Pinning facets[series][]=RWTC is what narrows a generic petroleum-spot-price query down to WTI specifically.

Putting the four knobs together produces exactly the request the adapter builds, plus the standard “latest row” tail:

GET https://api.eia.gov/v2/petroleum/pri/spt/data ?api_key=YOUR_KEY &frequency=daily &data[0]=value &facets[series][]=RWTC &sort[0][column]=period &sort[0][direction]=desc &offset=0 &length=1

The adapter prefers the unit EIA returns on the row (units) over the configured unit, so the on-chain label stays in sync with the source if EIA changes it.

Why a feed_id encodes the full tuple

A feed_id such as EIA.PET.PRICE.WTI.DAILY is not just a name. It is a stable alias for one exact { route, frequency, column, facets } tuple. The reason it must carry all four parts is that changing any single knob, while holding the others fixed, gives you a genuinely different number.

Same route, frequency, and column. The facet alone changes the asset:

FeedRouteFrequencyColumnFacet series
EIA.PET.PRICE.WTI.DAILYpetroleum/pri/sptdailyvalueRWTC (WTI)
EIA.PET.PRICE.BRENT.DAILYpetroleum/pri/sptdailyvalueRBRTE (Brent)

Same route, frequency, and facets. The column alone changes the metric:

FeedRouteFacetsColumn
EIA.ELEC.RETAIL.PRICE.US_ALL.MONTHLYelectricity/retail-salesstateid=US, sectorid=ALLprice
EIA.ELEC.RETAIL.SALES.US_ALL.MONTHLYelectricity/retail-salesstateid=US, sectorid=ALLsales
EIA.ELEC.RETAIL.REVENUE.US_ALL.MONTHLYelectricity/retail-salesstateid=US, sectorid=ALLrevenue

In both cases the difference between feeds is a single knob, yet the values are unrelated (a crude benchmark versus another crude benchmark; a price versus a volume versus a revenue). The identity of a number, what it actually measures, lives in the whole tuple, not in the route. Encoding the tuple into the feed_id is what lets an on-chain consumer reference EIA.PET.PRICE.WTI.DAILY as a short, immutable string instead of a fragile URL full of query parameters. The feed’s unit, decimals, title, source, route, and frequency are then recorded as self-describing metadata in the Feed Registry. See feeds and the registry for the metadata side.

Finding the right facet values

Knowing that you need facets[series][]=... is only half the job; you still need the correct code, like RWTC. There are three ways to find it, in order of convenience.

Claros ships the whole EIA surface as a static catalog at web/lib/eia-catalog.json (232 datasets under the leaves[] key). Each leaf lists the route, a name, a description, the supported frequencies with their period formats, the columns, and the facets with human descriptions:

{ "route": "electricity/retail-sales", "name": "Electricity Sales to Ultimate Customers", "description": "Electricity sales to ultimate customer by state and sector ...", "frequencies": [ { "id": "monthly", "format": "YYYY-MM" }, ... ], "columns": [ "revenue", "sales", "price", "customers" ], "facets": [ { "id": "stateid", "description": "State / Census Region" }, { "id": "sectorid", "description": "Sector" } ], "defaultFrequency": "monthly", "startPeriod": "2001-01", "endPeriod": "2026-04" }

The facet description tells you what a facet means; this is enough to pick the right facet id. You can browse all 232 datasets visually in the app at /datasets, and the 38 live feeds at /feeds.

From tuple to reading

Once a feed_id is defined and attested, you never touch EIA again to read it. The four knobs were resolved once, off-chain, by the agent; consumers just ask Claros for the feed by id. Pick the access path that fits your context:

For how readings land on-chain, the integer convention, and the gas note on Casper testnet, see the network reference.

Last updated on