Liking cljdoc? Tell your friends :D

clj-gridx

Clojars Project md-docs build-provenance

A Clojure client library for the GridX Pricing API, providing access to marginal cost pricing data for California utilities (PG&E and SCE). Built on a non-official OpenAPI spec derived from GridX's public developer docs.

Features

  • Multi-utility support: PG&E and SCE via parallel gridx.pge.client and gridx.sce.client namespaces
  • Spec-driven HTTP client built on Martian with bundled OpenAPI specs as the single source of truth
  • Two-layer data model: raw API responses (camelCase, strings) and coerced Clojure entities (namespaced keywords, BigDecimals, Instants)
  • tick intervals for time periods, enabling Allen's interval algebra out of the box
  • Metadata preservation: every coerced entity carries the original API data as :gridx/raw metadata
  • Malli schemas for both raw and coerced data layers

Installation

Add to your deps.edn:

{:deps {energy.grid-coordination/clj-gridx {:mvn/version "0.3.1"}}}

Quick Start

PG&E

(require '[gridx.pge.client :as pge]
         '[gridx.pricing :as pricing])

;; Create a PG&E client (defaults to stage API)
(def c (pge/create-client))

;; Or target production
(def c (pge/create-client {:url pge/production-url}))

;; Fetch pricing data — utility/market/program are filled in automatically
(def resp (pge/get-pricing c
            {:startdate "20260308"
             :enddate "20260308"
             :ratename "EELEC"
             :representativeCircuitId "013532223"}))

(pricing/success? resp)  ;=> true
(pricing/curves resp)    ;=> vector of coerced Curve maps

SCE

(require '[gridx.sce.client :as sce]
         '[gridx.pricing :as pricing])

;; Create an SCE client (defaults to stage API)
(def c (sce/create-client))

;; Fetch pricing data
(def resp (sce/get-pricing c
            {:startdate "20250701"
             :enddate "20250701"
             :ratename "TOU-EV-9S"
             :representativeCircuitId "System"}))

(pricing/success? resp)  ;=> true
(pricing/curves resp)    ;=> vector of coerced Curve maps

Shared Client

The utility-specific namespaces wrap the shared gridx.client namespace, which can also be used directly:

(require '[gridx.client :as client])

(def c (client/create-client {:url "https://pge-pe-api.gridx.com/stage/v1"
                               :spec-path "gridx-pricing-spec/pge/openapi.yaml"}))

(client/get-pricing c {:utility "PGE" :market "DAM" :program "CalFUSE" ...})

Utility Differences

The coercion layer (gridx.pricing) is shared — both utilities produce the same Clojure entity shape. The differences are in the API parameters and price component vocabulary:

AspectPG&ESCE
Client namespacegridx.pge.clientgridx.sce.client
Circuit parameter:representativeCircuitId (9-digit feeder ID; see circuit lookup):representativeCircuitId (substation name, e.g. "System")
Rate namesAG-A1, B6, EELEC, EV2AS, ...TOU-GS-1, TOU-EV-9S, TOU-PRIME, ...
Components per interval3 (cld, mec, mgcc)8 (abank, bbank, circuitpricecurve, mec, nbc, ppf, ramp, transmissionpricecurve)
Price typesgeneration, distributiongeneration, distribution, nonbypassablecharge, transmission
CCA supportYes (optional :cca param)No
Data available from2024-06-012025-07-01

PG&E Circuit ID Lookup

PG&E's representativeCircuitId is a 9-digit distribution feeder identifier. PG&E presents customers with a dropdown of these opaque numbers with no indication of what or where they are. The gridx.pge.circuits namespace maps all 98 known circuit IDs to their substation locations.

(require '[gridx.pge.circuits :as circuits])

;; Find circuit IDs by substation name (case-insensitive)
(circuits/find-circuits "mountain view")
;=> (["082031112" {:region "South Bay and Central Coast"
;                   :division "De Anza"
;                   :substation "MOUNTAIN VIEW"
;                   :feeder "1112"
;                   :in-gridx-enum? true}])

;; Look up a specific circuit
(circuits/circuit-location "013532223")
;=> {:region "Bay Area", :division "Diablo", :substation "LAKEWOOD", ...}

;; Browse by region
(keys (circuits/circuits-by-region))
;=> ("Bay Area" "Central Valley" "North Coast" ...)

;; Only circuits confirmed in the GridX API (59 of 98)
(count (circuits/gridx-circuits))  ;=> 59

Data derived from the PG&E 2022 Grid Needs Assessment (CPUC filing 496629893, Appendix D) and the Priicer community cross-reference.

Data Model

The library provides two views of the API data:

Raw Layer

Direct from the JSON — camelCase keys, string values. Useful for debugging or when you need the exact API representation.

(pricing/raw-curves resp)
;=> [{:priceHeader {:priceCurveName "PGE-CalFUSE-EELEC-SECONDARY"
;                    :marketName "CAISO-DAM"
;                    :startTime "2026-03-08T00:00:00-0800"
;                    ...}
;     :priceDetails [{:startIntervalTimeStamp "2026-03-08T00:00:00-0800"
;                     :intervalPrice "0.032176"
;                     :priceStatus "Final"
;                     :priceComponents [{:component "cld"
;                                        :intervalPrice "0.000351"
;                                        :priceType "distribution"} ...]}
;                    ...]}]

Coerced Layer

Idiomatic Clojure — namespaced keywords, native types, tick intervals. The same shape for both PG&E and SCE.

(first (pricing/curves resp))
;=> #:gridx.curve{:name "PGE-CalFUSE-EELEC-SECONDARY"
;                  :market :gridx.market/caiso-dam
;                  :interval-minutes 60
;                  :currency :USD
;                  :unit :kWh
;                  :start #time/offset-date-time "2026-03-08T00:00-08:00"
;                  :end #time/offset-date-time "2026-03-08T23:59:59-07:00"
;                  :period #:tick{:beginning #time/instant "2026-03-08T08:00:00Z"
;                                 :end #time/instant "2026-03-09T06:59:59Z"}
;                  :record-count 23
;                  :intervals [...]}

Curve

KeyTypeDescription
:gridx.curve/nameStringPrice curve name (e.g. "PGE-CalFUSE-EELEC-SECONDARY")
:gridx.curve/marketKeywordMarket identifier (e.g. :gridx.market/caiso-dam)
:gridx.curve/interval-minutesintInterval length: 15 or 60
:gridx.curve/currencyKeywordSettlement currency (e.g. :USD)
:gridx.curve/unitKeywordSettlement unit (e.g. :kWh)
:gridx.curve/startOffsetDateTimeCurve start in market-local time
:gridx.curve/endOffsetDateTimeCurve end in market-local time
:tick/beginningInstantCurve start as UTC Instant (tick interval key)
:tick/endInstantCurve end as UTC Instant (tick interval key)
:gridx.curve/record-countintNumber of intervals
:gridx.curve/intervalsvectorVector of Interval maps

Interval

KeyTypeDescription
:tick/beginningInstantInterval start as UTC Instant (tick interval key)
:tick/endInstantInterval end as UTC Instant (tick interval key)
:gridx.interval/priceBigDecimalTotal interval price in currency/unit
:gridx.interval/statusKeyword:gridx.status/final or :gridx.status/preliminary
:gridx.interval/componentsvectorVector of Component maps

Component

KeyTypeDescription
:gridx.component/nameKeyworde.g. :gridx.component/cld, :gridx.component/mec, :gridx.component/abank
:gridx.component/priceBigDecimalComponent price
:gridx.component/typeKeyworde.g. :gridx.price-type/generation, :gridx.price-type/distribution, :gridx.price-type/transmission

PG&E components: cld (distribution), mec (generation), mgcc (generation)

SCE components: abank (distribution), bbank (distribution), circuitpricecurve (distribution), mec (generation), nbc (nonbypassablecharge), ppf (generation), ramp (generation), transmissionpricecurve (transmission)

Type Coercion Summary

Raw (API)Coerced (Clojure)Example
Timestamp stringjava.time.Instant (UTC)"2026-03-08T00:00:00-0800"#time/instant "2026-03-08T08:00:00Z"
Timestamp string (curve bounds)java.time.OffsetDateTime"2026-03-08T00:00:00-0800"#time/offset-date-time "2026-03-08T00:00-08:00"
Decimal stringBigDecimal"0.032176"0.032176M
Enum stringNamespaced keyword"Final":gridx.status/final

Time Handling

Time representation is chosen by semantics:

  • Interval timestamps use Instant (UTC) — these are point-in-time price observations that must be globally unambiguous
  • Curve start/end use OffsetDateTime — these represent calendar boundaries in the market's local time. The offset conveys market context (e.g., -08:00 PST vs -07:00 PDT)

The library never assumes a timezone. Offsets cannot be converted to zone IDs without external knowledge (-08:00 could be US/Pacific, US/Alaska, etc.). If you know the zone, convert explicitly:

(.atZoneSameInstant (:gridx.curve/start curve)
                    (java.time.ZoneId/of "America/Los_Angeles"))

Tick Intervals

Both Curve and Interval entities carry :tick/beginning and :tick/end directly, making them tick intervals usable with Allen's interval algebra:

(require '[tick.core :as t])

(let [intervals (:gridx.curve/intervals (first curves))
      i1 (nth intervals 0)
      i2 (nth intervals 1)
      i3 (nth intervals 2)]

  (t/relation i1 i2)      ;=> :meets
  (t/relation i1 i3)      ;=> :precedes

  ;; Access interval boundaries directly
  (:tick/beginning i1)     ;=> #time/instant "2026-03-08T08:00:00Z"
  (:tick/end i1))          ;=> #time/instant "2026-03-08T09:00:00Z"

Note on curve tick/end: The GridX API reports curve end time as 23:59:59 (inclusive convention), while tick intervals are half-open [start, end). This means the curve's :tick/end is 1 second before the last interval's computed end time. The library preserves the API's value faithfully and does not adjust for this difference.

Metadata

Every coerced entity preserves the original API data as metadata, accessible via :gridx/raw:

;; Get a coerced interval
(def interval (-> curves first :gridx.curve/intervals first))

;; Access the original API data
(-> interval meta :gridx/raw)
;=> {:startIntervalTimeStamp "2026-03-08T00:00:00-0800"
;    :intervalPrice "0.032176"
;    :priceStatus "Final"
;    :priceComponents [{:component "cld"
;                       :intervalPrice "0.000351"
;                       :priceType "distribution"} ...]}

This works at every level — curves, intervals, and components all carry their raw data.

Schemas

Malli schemas are published in dedicated namespaces so consumers can use them for validation, generative testing, or documentation without pulling in coercion machinery.

gridx.pricing.schema — Coerced entities (the public contract)

(require '[gridx.pricing.schema :as schema]
         '[malli.core :as m])

;; Validate a coerced curve
(m/validate schema/Curve (first curves))
;=> true

;; Available schemas: Component, Interval, Curve

gridx.pricing.schema.raw — Raw API shapes

Most consumers won't need these. They mirror the JSON exactly and are primarily useful for boundary validation.

(require '[gridx.pricing.schema.raw :as schema.raw])

;; Validate a raw API response body
(pricing/validate-raw (:body resp))
;=> nil  (success — returns nil on valid, Malli explanation map on failure)

;; Available schemas: PriceComponent, PriceDetail, PriceHeader,
;;                    PriceCurve, ResponseMeta, PricingResponse

API Reference

gridx.pge.client — PG&E

FunctionDescription
create-clientCreate a PG&E client. Options: :url (default: stage), :spec-path
get-pricingFetch PG&E pricing. Fills in utility/market/program. Params: :startdate, :enddate, :ratename, :representativeCircuitId, :cca (optional)
stage-urlPG&E stage API base URL
production-urlPG&E production API base URL

gridx.sce.client — SCE

FunctionDescription
create-clientCreate an SCE client. Options: :url (default: stage), :spec-path
get-pricingFetch SCE pricing. Fills in utility/market/program. Params: :startdate, :enddate, :ratename, :representativeCircuitId
stage-urlSCE stage API base URL
production-urlSCE production API base URL

gridx.pge.circuits — Circuit ID Lookup

FunctionDescription
circuit-locationsMap of all 98 circuit IDs to location info
circuit-locationLook up location for a circuit ID
find-circuitsSearch by substation name (case-insensitive substring)
circuits-by-regionGroup circuits by PG&E distribution planning region
gridx-circuitsReturn only circuits confirmed in the GridX API

gridx.client — Shared

FunctionDescription
create-clientCreate a client with explicit :url and :spec-path (both required)
get-pricingFetch pricing data with explicit params. Returns raw HTTP response
routesList available API route names

gridx.pricing

FunctionDescription
success?Check if an API response indicates success
raw-curvesExtract raw (uncoerced) curves from response
curvesExtract and coerce curves into Clojure entities
validate-rawValidate response body against raw Malli schema
->gridx-dateConvert a date to GridX YYYYMMDD format
->componentCoerce a raw component map
->intervalCoerce a raw price detail map (requires duration)
->curveCoerce a raw price curve map

gridx.pricing.schema

Malli schemas for the coerced Clojure entities — the public contract for consumers.

SchemaDescription
ComponentPrice component with BigDecimal price and type keyword
IntervalPrice interval with tick period, price, status, and components
CurveComplete price curve with header fields and vector of intervals

gridx.pricing.schema.raw

Malli schemas mirroring the raw JSON API shape. Primarily for boundary validation.

SchemaDescription
PriceComponentRaw component (component, intervalPrice, priceType)
PriceDetailRaw interval detail with timestamp, price, status, components
PriceHeaderRaw curve metadata (name, market, times, record count)
PriceCurveRaw curve (header + details vector)
ResponseMetaHTTP response metadata (code, URLs, body)
PricingResponseTop-level API response (meta + data vector)

REPL Session Example

A complete REPL session demonstrating the full workflow:

;; Setup
(require '[gridx.pge.client :as pge]
         '[gridx.sce.client :as sce]
         '[gridx.pricing :as pricing]
         '[gridx.pricing.schema :as schema]
         '[tick.core :as t]
         '[tick.alpha.interval :as t.i]
         '[malli.core :as m])

;; -- PG&E --
(def pc (pge/create-client))

(def pge-resp (pge/get-pricing pc
                {:startdate (pricing/->gridx-date (t/today))
                 :enddate   (pricing/->gridx-date (t/today))
                 :ratename "EELEC"
                 :representativeCircuitId "013532223"}))

(pricing/success? pge-resp)  ;=> true
(def pge-curves (pricing/curves pge-resp))
(m/validate schema/Curve (first pge-curves))  ;=> true

;; -- SCE --
(def sc (sce/create-client))

(def sce-resp (sce/get-pricing sc
                {:startdate "20250701"
                 :enddate "20250701"
                 :ratename "TOU-EV-9S"
                 :representativeCircuitId "System"}))

(pricing/success? sce-resp)  ;=> true
(def sce-curves (pricing/curves sce-resp))

;; SCE has 8 components per interval
(-> sce-curves first :gridx.curve/intervals first :gridx.interval/components count)
;=> 8

;; Find negative price hours (solar oversupply!)
(->> (:gridx.curve/intervals (first pge-curves))
     (filter #(neg? (:gridx.interval/price %)))
     (mapv (fn [i]
             {:begin (:tick/beginning i)
              :price (:gridx.interval/price i)})))

;; Interval algebra — entities are tick intervals directly
(let [intervals (:gridx.curve/intervals (first pge-curves))]
  (t/relation (nth intervals 0) (nth intervals 1)))
;=> :meets

;; Access raw API data from any coerced entity
(-> (first pge-curves) meta :gridx/raw :priceHeader :priceCurveName)
;=> "PGE-CalFUSE-EELEC-SECONDARY"

Development

Start nREPL

clojure -M:nrepl
# nREPL server started on port XXXXX on host localhost
# Port is written to .nrepl-port for automatic discovery

Dev helpers

The dev/user.clj namespace provides REPL convenience functions:

(start!)                                                           ; init both clients
(start-pge!)                                                       ; init PG&E client only
(start-sce!)                                                       ; init SCE client only
(fetch-pge-pricing "EELEC" "013532223" "20260308" "20260308")      ; PG&E quick fetch
(fetch-sce-pricing "TOU-EV-9S" "System" "20250701" "20250701")    ; SCE quick fetch

Run tests

# Unit tests (offline, uses bundled sample JSON)
clojure -M:test -m kaocha.runner

# Integration tests (requires network access to pe-api.gridx.com)
clojure -M:test-integration

Unit tests validate schema conformance and coercion logic against sample response files. Integration tests hit the live stage API and verify response structure, component names/counts, type coercion, and metadata preservation for both PG&E and SCE — without asserting specific price values.

License

MIT License — Copyright (c) 2026 Clark Communications Corporation

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close