# pjm-nowcast

PJM RTO nowcast: latest load, RTO LMP, and zonal LMPs from public page ingest.
Descriptive statistics only. Not a forecast, signal, or trading recommendation.

Base URL: https://pjm-nowcast-production.up.railway.app
MCP: POST https://pjm-nowcast-production.up.railway.app/mcp (alias https://pjm-nowcast-production.up.railway.app/api/mcp). Server card: https://pjm-nowcast-production.up.railway.app/.well-known/mcp/server-card.json. Streamable HTTP JSON-RPC. No key for tools/list.

Pay: x402 USDC exact on configured networks. Unpaid paid routes return HTTP 402; decode the PAYMENT-REQUIRED header. Unpaid MCP paid tools return paymentStatus=required.

Free:
- GET /
- GET /health
- GET /ready
- GET /openapi.json
- GET /swagger.json
- GET /llms.txt
- GET /llm.txt
- GET /v1/demo/sample — unpaid synthetic fixture for schema inspection only; not live PJM
- GET /v1/nowcast/weather-summary — zone weather sensitivities and latest residual
- GET /.well-known/x402
- GET /.well-known/mcp/server-card.json
- GET /.well-known/llms.txt

Paid (x402 USDC exact on configured networks):
- POST /v1/nowcast/latest — $0.02 — MCP nowcast_latest — Latest PJM RTO LMP, zonal spreads, RTO load, and nested regime snapshot
- POST /v1/nowcast/history — $0.10 — MCP nowcast_history — 1–72h PJM RTO LMP, zonal spread, and load history
- POST /v1/nowcast/history/extended — $0.25 — MCP nowcast_history_extended — Extended PJM history within the retention window
- POST /v1/nowcast/regime — $0.02 — MCP nowcast_regime — Proprietary latent-state snapshot with asOf and ageSeconds
- POST /v1/nowcast/spreads — $0.02 — MCP nowcast_spreads — Zone-level LMP spread table for the latest print
- POST /v1/nowcast/asof — $0.02 — MCP nowcast_asof — Descriptive snapshot pinned to a historical asOf
- POST /v1/nowcast/snapshots — $0.10 — MCP nowcast_snapshots — Append-only /latest snapshots by EPT hour
- POST /v1/nowcast/weather-sensitivity — 0.10 — MCP nowcast_weather_sensitivity — Weather sensitivity series, bands, and degree-day path
- POST /v1/spread/probability-card — 0.25 — MCP spread_probability_card — On-peak zonal spread jump probability card

PJM zonal spread jump probability. empirical probability card for the evening congestion bar.
whenToUse: next on-peak day or trailing congestion-season check.
whenNotToUse: month-ahead, season-ahead, Black vol, a price forecast.

## Directory reviewer path

1. `initialize` + `tools/list` on `POST /mcp`.
2. Free tools `health`, `service_info`, `demo_sample`, `nowcast_weather_summary`.
3. Unpaid `nowcast_weather_sensitivity` returns `paymentStatus=required`. Do not settle.

## When to call which route

- POST /v1/nowcast/latest: Need the current print, freshness, and 24h trailing stats. Start here before history, as-of, or the spread card.
- POST /v1/nowcast/history: Need a short trailing window (hours, not days) with native poll points. Use extended history for multi-day or hourly buckets.
- POST /v1/nowcast/history/extended: Need more than 72 hours, hourly buckets, or a prior-period comparison for a backtest. Cap is retention (default 90 days).
- POST /v1/nowcast/regime: Check regime confidence and the dominant state before quoting. Pair with /latest for the print itself.
- POST /v1/nowcast/spreads: Need this print's zone map and lastSpread without a history dump. Use /latest when you also need RTO LMP, load, and 24h stats.
- POST /v1/nowcast/asof: Pin a historical hour and compare it to /latest to judge whether the print drifted. Fail closed: do not interpolate.
- POST /v1/nowcast/snapshots: Monitor what /latest said each EPT hour, or join snapshots to settled actuals. Do not treat a missing hour as zero.
- POST /v1/nowcast/weather-sensitivity: Inspect the hourly weather join, residual band, and degree-day path. Do not treat the driven path as a market forecast.
- POST /v1/spread/probability-card: next on-peak day or trailing congestion-season check. Do not use for month-ahead, season-ahead, Black vol, a price forecast.

## Worked agent use cases

### 1. Check the current PJM East price regime before quoting

Goal: Confirm freshness and regime confidence before quoting a short-dated window.
Routes in order: POST /v1/nowcast/latest, POST /v1/nowcast/regime

Sample request:

```json
{"families": ["rto_lmp", "zonal_spread"]}
```

Sample response excerpt:

```json
{"asOf": "2026-10-09T18:00:00-04:00", "stale": false, "rtoLmp": {"unit": "USD/MWh", "last": 41.0}, "entropy": 1.1, "kStar": 4}
```

How to interpret: If stale is true, wait for a fresh poll. Lower entropy means a more decisive regime. Use rtoLmp.last as the current RTO mark, not as a forward. Then quote.

### 2. Size a spread hedge with the on-peak probability card

Goal: Turn recent on-peak zonal-spread jump frequency into a hedge size.
Routes in order: POST /v1/nowcast/latest, POST /v1/spread/probability-card

Sample request:

```json
{"lookbackDays": 5}
```

Sample response excerpt:

```json
{"sample": {"tag": "high-congestion-season", "dayCount": 5}, "K": 12.4, "hitRate": 0.4, "avgPayoff": 1.8}
```

How to interpret: K is the quiet high-water mark in USD/MWh. hitRate is empirical frequency on the named sample, not a forecast. avgPayoff is mean positive excess over K. Size the short-dated spread hedge off hitRate and avgPayoff; do not treat K as a mid or as Black vol.

### 3. Compare the live nowcast to an as-of pin for drift

Goal: Judge whether the current print moved versus a pinned hour.
Routes in order: POST /v1/nowcast/latest, POST /v1/nowcast/asof

Sample request:

```json
{"asOf": "2026-10-09T18:00:00-04:00"}
```

Sample response excerpt:

```json
{"asOf": "2026-10-09T18:00:00-04:00", "pinnedAsOf": "2026-10-09T18:00:00-04:00", "rtoLmp": {"unit": "USD/MWh", "last": 41.0}}
```

How to interpret: Subtract pinned rtoLmp.last (and lastSpread) from the live /latest print. A large gap is realized drift of the tape, not a model forecast. asof fails closed: if the hour is missing, there is no row to compare.

### 4. Pull extended history for a backtest

Goal: Build a trailing realized window with hourly buckets and a prior period.
Routes in order: POST /v1/nowcast/history/extended

Sample request:

```json
{"families": ["rto_lmp", "zonal_spread"], "windowHours": 168, "resolution": "hourly", "compare": "prior_period"}
```

Sample response excerpt:

```json
{"asOf": "2026-10-09T18:00:00-04:00", "ageSeconds": 120, "stale": false, "windowHours": 168, "resolution": "native", "rtoLmp": {"unit": "USD/MWh", "n": 200, "last": 41.0}}
```

How to interpret: windowHours is the trailing length. resolution=hourly averages native polls per clock hour. priorPeriod is the equal-length window immediately before. Stats are descriptive; do not treat mean/std as a live forecast.

### 5. Monitor intraday nowcast snapshots

Goal: See what /latest reported each EPT hour, optionally with this-print zones.
Routes in order: POST /v1/nowcast/snapshots, POST /v1/nowcast/spreads

Sample request:

```json
{"from": "2026-10-09T00:00:00-04:00", "to": "2026-10-10T00:00:00-04:00"}
```

Sample response excerpt:

```json
{"disclaimer": "Descriptive statistics only. Not a forecast, signal, or trading recommendation. Aggregated from public market sources. Freshness depends on the last successful background poll. Append-only snapshots of what /latest reported for that EPT hour-beginning. Not a forward forecast of a future hour. Hours without a row have no stored nowcast (null, not zero). Residuals are only defined where a snapshot exists.", "windowDays": 90, "from": "2026-10-09T00:00:00-04:00", "to": "2026-10-10T00:00:00-04:00", "count": 1, "snapshots": [{"asof": "2026-10-09T18:05:00-04:00", "target_hour_ept": "2026-10-09T18:00:00-04:00", "rto_nc": 41.0, "spr_nc": 5.1, "zone_nc": {"BGE": 46.0}}]}
```

How to interpret: Each row is the first /latest write for that EPT hour-beginning. A missing hour is null, not zero. spr_nc is the stored nowcast spread, not settled spr_act. Pair with /spreads for this print's zone map.

On 402, decode PAYMENT-REQUIRED, settle x402 exact USDC, retry the same POST with PAYMENT-SIGNATURE.

Paid payloads are descriptive last/min/max/mean/std/percentiles for RTO LMP (USD/MWh), zonal LMP spread (USD/MWh, plus zone map), and RTO load (MW), with asOf/polledAt/stale. Trailing mean/std is a mix of the stored sample, not a live forecast F. Do not treat that mix as live F while entropy is near ln(5) (~1.609); the internal state mix is then uninformative. POST /v1/nowcast/latest and MCP nowcast_latest also return last price_vol ($/MWh rolling realized LMP std over the last 8 prints, RMS of successive diffs — same formula as the poller, not annualized Black vol, not rtoLmp.std / mix std) and price_vol_missing. After restart, last price_vol is rebuilt from those SQLite RTO LMP diffs when the in-memory window is empty. When vol is missing, non-finite, or <= 0, price_vol_missing is true and price_vol is null. Latest also returns mix_std_price (model-implied dispersion of RTO LMP, $/MWh, proprietary) and mix_n_obs from mix.json; that is not price_vol. POST /v1/nowcast/latest also returns nested regime: entropy (uncertainty of the state estimate, nats), posteriors (regime state probabilities), kStar (most likely state), and stateMeans (average conditions in each state, raw MW and USD/MWh). Same entropy/posteriors/kStar as POST /v1/nowcast/regime. high-spread is an internal zonal flag, not returned on HTTP/MCP nowcast bodies.

OpenAPI: https://pjm-nowcast-production.up.railway.app/openapi.json
