Quickstart.

Amanoki answers three questions about a wholesale electricity market area, over plain HTTP. What state is it in right now — is price moving unusually little, normally, unusually much, or is it short of supply? How often has price gone on to cross a given level within the next two or four hours, starting from a moment like this one? And how likely is each of those four states one, two, or four hours from now? JEPX (Japan) is live, with every regional area plus the system price; the other catalogued markets — ERCOT, PJM, CAISO, AEMO, NYISO at the time of writing — are routed and answer that they are in warmup. GET /v1/markets is the catalog itself, and the table on the front page renders it.

Every endpoint listed here is callable without an API key today — the service is in public preview while the billing stack is wired. Paid tiers will change the rate limit, not the shape.

The first call

This is the whole of it. No key, no signup:

curl https://api.amanoki.com/v1/power/jepx/tokyo

{"market":"jepx","area":"tokyo","currency":"JPY","price_unit":"kWh",
 "regime":"low","price":20.77,"z_vol":-1.17,
 "realized_vol":…,"baseline_vol":…,
 "last_update_ms":1785335400000}

Read it as: for the 30-minute settlement interval starting at last_update_ms, JEPX cleared the Tokyo area at ¥20.77/kWh, and that interval is labelled low. z_vol is how far this interval's volatility sits from its own recent baseline, in standard deviations — negative means quieter than usual — and the label is derived from it. Two things about the timestamp: it is UTC milliseconds, and it names the start of the interval being described rather than the moment we fetched it, so for a day-ahead market it usually sits several hours in the future. Swap tokyo for any other JEPX area, or for systemGET /v1/markets/jepx/areas lists the names its URLs accept. realized_vol and baseline_vol are the two volatility figures z_vol is built from, carried so you can recompute the z-score yourself.

Before an area has enough history to compute a baseline, the same endpoint answers {"regime":"normal","warmup":true,"message":…} — check warmup before trusting a label. That is also what every catalogued-but-not-yet-connected market returns.

HTTP (primary surface)

Base URL is https://api.amanoki.com; everything below is a GET under /v1. In the order they are most useful: what exists, whether the service is healthy, the current label, the forecast, the spike table, the recent state changes, and which neighbouring areas tend to move first.

curl https://api.amanoki.com/v1/markets
curl https://api.amanoki.com/v1/status
curl https://api.amanoki.com/v1/power/jepx/tokyo
curl "https://api.amanoki.com/v1/power/jepx/tokyo/regime-forecast?horizon_min=120"
curl https://api.amanoki.com/v1/power/jepx/tokyo/spike-probability
curl "https://api.amanoki.com/v1/power/jepx/tokyo/transitions?limit=10"
curl "https://api.amanoki.com/v1/power/jepx/tokyo/spatial-influence?target_regime=high"

horizon_min is one of 60 / 120 / 240. spatial-influence ranks the other JEPX areas by how often their entry into target_regime has preceded this area's, which is why normal is rejected there — it carries no information to precede anything with. The fuel-cost adjustment lives on its own endpoints under /v1/nencho/, described on fuel adjustment.

Python SDK

The amanoki package wraps the same endpoints, one method per URL. Most methods return a typed dataclass you read by attribute; regime_forecast, transition_matrix, regime_durations, history, spatial_influence and health return the decoded JSON, which you read by key. Both appear in the block below on purpose, and the SDK page marks every method. Raw HTTP callers see the same JSON content either way.

pip install amanoki

from amanoki import Client

c = Client()

# SDK style (typed dataclass)
r = c.get_regime("jepx", "tokyo")
print(r.regime, r.price, r.price_unit, r.z_vol)

# Equivalent raw-HTTP shape (dict access)
# r = requests.get("https://api.amanoki.com/v1/power/jepx/tokyo").json()
# print(r["regime"], r["price"], r["price_unit"], r["z_vol"])

s = c.spike_probability("jepx", "tokyo")
for cell in s.cells:
    print(cell.threshold, cell.horizon_min, cell.p, cell.base_rate)

# regime_forecast returns the raw dict, so read it by key
f = c.regime_forecast("jepx", "tokyo", horizon_min=120)
print(f["method"], f["probabilities"], f.get("test_brier"))

ts = c.transitions("jepx", "tokyo", limit=10)
for t in ts:
    print(t.ts_ms, t.from_regime, "->", t.to_regime, "@", t.price)

Async flavour via amanoki.AsyncClient. Full reference at /v1/reference.

Response shape (regime-forecast)

{
  "market": "jepx",
  "area": "tokyo",
  "horizon_min": 120,
  "asof_ms": 1776695400000,
  "method": "weather_conditioned_mlr_v1_5",
  "sample_size_bars": 36000,
  "probabilities": {
    "low": 0.006, "normal": 0.994, "high": 0.0, "scarcity": 0.0
  },
  "base_rates": {
    "low": 0.075, "normal": 0.718, "high": 0.207, "scarcity": 0.0
  },
  "test_brier": 0.0321,
  "baseline_test_brier": 0.4319,
  "weather_fetched_ms": 1776604192971
}

sample_size_bars counts the 30-minute bars behind the table for this one (market, area) — Tokyo alone in the response above. It grows as the daily archive refresh extends the history; a JEPX area currently carries a little over 40,000 bars, a bit more than two years.

probabilities is the model's prediction at t + horizon_min; base_rates is the unconditional class frequencies from training (context for the model's confidence). test_brier against baseline_test_brier is the held-out calibration comparison: 0.0321 against a 0.4319 baseline in the response above. Both are the scores the model earned at training time on its held-out split, not a rolling score on live outcomes. A Brier score is the mean squared distance between the four predicted probabilities and the outcome that occurred, so 0 is perfect and lower is better; /stats carries the current figures per horizon.

The two timestamps mean different things and are meant to look far apart. asof_ms is the start of the settlement interval the forecast is anchored on, and JEPX is a day-ahead market, so it normally sits hours in the future. weather_fetched_ms is wall-clock: when the weather inputs were last pulled, on an hourly refresh. So roughly a day between them is the day-ahead offset rather than a stale cache. When the weather cache does fall behind — more than two hours — the response carries weather_staleness_warning and serving continues.

Response shape (spike-probability)

{
  "market": "jepx",
  "area": "tokyo",
  "hour_of_day_local": 23,
  "is_weekend": false,
  "bucket_size": 1072,
  "method": "empirical_by_hour_and_weekend",
  "sample_size_bars": 36000,
  "cells": [
    {"threshold": 30.0, "horizon_min": 120, "p": 0.0036, "base_rate": 0.014},
    {"threshold": 80.0, "horizon_min": 240, "p": 0.0,    "base_rate": 0.0002}
  ]
}

p is the empirical probability that the maximum price over the next horizon_min minutes crosses threshold, conditioned on the current hour-of-day and weekend flag. bucket_size is how many historical bars fed into that conditional estimate — cells with small buckets (< 100) are noisy and should be discounted.

Error handling

The SDK raises rather than returns on any non-2xx response, so a caller only has to catch the cases it can act on.

import time

from amanoki import Client, NotFoundError, RateLimitError

try:
    c.get_regime("jepx", "okinawa")  # area not tracked
except NotFoundError:
    ...
except RateLimitError as e:
    time.sleep(e.retry_after or 30)

All error responses ship as application/problem+json (RFC 7807) with {type, title, status, detail, instance}. The type URI points at the relevant section of the methodology so you can read past the symptom to the cause.