Skip to content

API reference

The GRIDRUN server is at api.gridrun.net. Game logic lives in a pure deterministic core; the API is the delivery layer.

Prefer to explore interactively? Open the interactive API reference - a live viewer over the generated OpenAPI spec.

Getting started

A programmatic client needs two credentials: an API key and a seat bearer token. Both are obtained by a human through the dashboard at gridrun.net.

Human setup (one-time, via the dashboard):

  1. Log in at gridrun.net with your email (OTP code is sent to you).
  2. Browse the lobby and join a seat -- pick a role and region.
  3. Request an API key from the dashboard. Keys require approval. Choose access level: play (default) or full (also join/vacate seats programmatically).
  4. Reveal the key within 72 hours of approval (one-time window).
  5. Give the API key and seat bearer token to your programmatic client.

play keys can resume their seat, issue commands, read state, and stream. full keys can additionally browse the lobby, join, accept, and vacate.

Authentication

Every request requires both headers:

HeaderValue
X-API-Keygr_live_... (your approved API key)
AuthorizationBearer <seat JWT>

For SSE (EventSource cannot set headers), pass as query parameters: ?token=<token>&apiKey=<key>.

Game time: 1 tick = 15 game-minutes; 96 ticks = 1 game-day; 336 game-days = 1 game-year. At the default production cadence (60s/tick): 1 game-day ~ 96 real-minutes, 1 game-year ~ 22 real-days. Query GET /v1/rules for live values.

Action budget: 1 action per game-day (96 ticks), capped at 10. Commands cost 1 action; dry-runs and reads are free.

API key expiry: 14 days (336 hours) by default. Each accepted command renews the expiry window. Reads, streams, and lobby calls do not renew.


Endpoints

GET /v1/seat

Your seat: role, cash, budget, assets, role state, queued actions, score.

Scope: read

json
{
  "seatId": "retailer-B-56",
  "role": "retailer",
  "regionId": "B",
  "status": "active",
  "cash": 5110380,
  "budget": { "current": 10, "cap": 10 },
  "assets": [
    { "id": "ccgt-A-0", "type": "ccgt", "nameplateMW": 200, "condition": 99.8, "age": 12, "status": "active" }
  ],
  "roleState": { "role": "retailer", "retailer": { "tariffType": "indexed", "hedgeFrac": 0.5 } },
  "actionQueue": [
    { "id": "q-96-gov-0", "command": { "type": "gov.SetCarbonTax", "params": { "value": 75 } }, "fireAtTick": 1200, "predicate": null }
  ],
  "score": {
    "standing": 49.1,
    "daysInSeat": 3,
    "daily": [{ "gameDay": 0, "score": 43.7, "components": { "pnl": 1200000, "servedMwh": 9000000 } }],
    "factors": { "segment_households": 0.338 }
  }
}

GET /v1/state

Public market state: current tick, regions, leaderboard.

Scope: read

json
{
  "tick": 1200,
  "paused": false,
  "pendingEpoch": null,
  "regions": [
    { "id": "A", "name": "Builn", "price": 55.0, "capMult": 1.0, "fuelPriceThermal": 26.3 },
    { "id": "B", "name": "Solhaven", "price": 48.2, "capMult": 1.0, "fuelPriceThermal": 26.3 },
    { "id": "C", "name": "Portmere", "price": 49.3, "capMult": 1.0, "fuelPriceThermal": 26.3 }
  ],
  "leaderboard": [
    { "seatId": "retailer-B-56", "role": "retailer", "standing": 49.1, "handle": "52774788", "daysInSeat": 3 }
  ]
}

POST /v1/commands

Issue a game command. Idempotent by key. Supports dry-run preview.

Scope: command . Cost: 1 action (free if dryRun: true)

Request:

json
{
  "idempotencyKey": "my-cmd-1",
  "type": "gov.SetCarbonTax",
  "params": { "value": 50 },
  "dryRun": false
}

Accepted:

json
{ "status": "accepted", "commandId": "cmd-136", "appliedAtTick": 1200, "budgetRemaining": 9 }

Dry-run preview:

json
{
  "status": "preview",
  "commandId": "cmd-135",
  "appliedAtTick": 1200,
  "budgetRemaining": 10,
  "preview": [{ "seatId": "gov-A-0", "changes": { "gov.policy.carbonTax": { "from": 0, "to": 50 } } }]
}

Rejected (HTTP 200 -- business rejection, not a transport error):

json
{
  "status": "rejected",
  "reason": "OUT_OF_BOUNDS",
  "detail": "parameter \"value\" = 200 is above maximum 150",
  "commandId": "cmd-137",
  "appliedAtTick": 1200,
  "budgetRemaining": 10
}

Rejection reasons: BAD_PARAMS, INSUFFICIENT_BUDGET, OUT_OF_BOUNDS, PRECONDITION_FAILED, SEAT_NOT_ACTIVE, DOES_NOT_OWN_ASSET, INSUFFICIENT_CASH, UNKNOWN_COMMAND, UNKNOWN_SEAT.

The detail field (when present) names the offending parameter and the expected type, range, or enum values.

GET /v1/stream

Live SSE event stream. Connect with EventSource.

Scope: stream . Content-Type: text/event-stream

event: hello
data: {"seatId":"retailer-B-56","tick":1200}

event: tick
data: {"tick":1201,"prices":{"A":55.1,"B":48.0,"C":49.5}}

event: budget
data: {"current":9,"cap":10}

event: bulletin
data: {"tick":1201,"code":"asset.outage","params":{"assetId":"...","duration":4},"source":"game"}

Heartbeat comment (: hb) every 15 seconds.

GET /v1/leaderboard

Scope: read -> { leaderboard: [...] } (same shape as /v1/state)

GET /v1/bulletin

Bulletin history: emitted events + operator bulletins + rolling history.

Scope: read -> { tick, emittedEvents: [...], bulletinHistory: [...], operatorBulletins: [...] }

GET /v1/rules

Machine-readable config, command types, and full param schemas.

Scope: read -> { config, priceCapMWh, ticksPerDay: 96, commandTypes: [...], commandSchemas: [...] }

The commandSchemas array contains one entry per registered command with parameter names, types, ranges, enum values, and descriptions -- sufficient for a headless client to validate locally without guessing.


Command catalog

All commands cost 1 action unless noted. Temporal levers are marked: (ramp) = phases in over game-days; (tenor) = carries a term parameter.

Government

CommandParamsDescription
gov.SetCarbonTaxvalue ($/tCO2, 0-150)Carbon price
gov.SetRetailPriceCapvalue ($/MWh, 0-500)Retail ceiling
gov.SetRenewableSubsidyvalue ($/MWh, 0-100)Clean generation subsidy
gov.SetCapacityPaymentvalue ($/MW-day, 0-20)Capacity market payment
gov.SetImportTariffvalue (0-0.5)Import tariff fraction (0-50%)
gov.SetInterventionStancestance (active | passive)Intervention mode
gov.BuyStrategicReservecoverageDays (1-30)Buy reserve
gov.NationalisetargetSeatIdTake over a distressed seat (cash < 0, active stance)
gov.SanctiontargetSeatIdPenalise a seat's standing (-30 in your region). Costs your governance score
gov.LiftSanctiontargetSeatIdRestore a sanctioned seat's access

Retailer

CommandParamsDescription
retailer.SetTarifftariffType (flat | indexed | tou), flatPrice?, indexedMargin?, touPeakMult?, touOffPeakMult?Tariff type + parameters
retailer.SetHedgeFracvalue (0-1)Forward hedge fraction
retailer.SetSegmentOffsetsegment (households | commercial | industrial | ev | municipal), pct (-0.15 to 0.15)Per-segment price offset

Genco

CommandParamsDescription
genco.SetBidMarkupvalue (0-0.5)Bid markup (ramp)
genco.SetForwardSellfrac (0-1), tenorTicks (672-35040)Forward sell (tenor)
genco.SetFuelHedgefrac (0-1)Fuel hedge fraction
genco.BuildPlant:ccgt--Build CCGT (20 game-days)
genco.BuildPlant:peaker--Build peaker (7 game-days)

Renewable

CommandParamsDescription
renewable.SignPPAvolMW (0-1), price ($20-80/MWh), tenorTicks (672-35040)PPA contract (tenor)
renewable.BuildFarm:wind--Build wind (14 game-days)
renewable.BuildFarm:solar--Build solar (7 game-days)

Storage

CommandParamsDescription
storage.SetDispatchPolicychargeBelow (0-2), dischargeAbove (0-3)Arbitrage thresholds (x avg price)
storage.SetAncillaryCommitmw (0-200), tenorTicks (672-35040)Ancillary commit (tenor)
storage.BuildStorage--Build battery (7 game-days)

Fuel

CommandParamsDescription
fuel.SetOfftakefrac (0-1), tenorTicks (672-35040, i.e. 7-365 game-days)Offtake contract (tenor)
fuel.SetStandingBudgetrate ($/game-day, 0-100000)Standing budget (debited daily open)
fuel.BuildInfrastructurelevel (2-3, must = current+1)Infrastructure upgrade (single-step)

Materials

CommandParamsDescription
materials.SetContractFracfrac (0-1)Contract fraction
materials.SetDepletionRaterate (1-20)Extraction rate
materials.SpendCommunityamount ($, >=0)Community spend (repair-only: raises popularity to 70 max, charges the effective delta)

Manufacturer

CommandParamsDescription
manufacturer.SetConversionCapacityunits (10-200)Daily capacity
manufacturer.SetEquipmentPriceprice ($50k-200k)Equipment price
manufacturer.RewiretargetGen (1-8)Retool production line

Trading House

CommandParamsDescription
trader.PlaceOrdervolume (100-10000)Place a trade order (max 10 open positions)
trader.SellForwardleg (fuel | power), volume (100-10000), tenorTicks (672-35040)Forward contract (tenor)
logistics.SetRedundancyrouteKey, spareMW (0-5000)Route redundancy (increases queue with 3-game-day lead)

Common

CommandParamsCostDescription
common.SetMaintenanceBudgetassetId, rate (0-50000 $/tick)1Maintenance spend (max $50,000/tick per asset)
common.EmergencyOverhaulassetId1$1.5M instant repair
common.ResignSeat--1Resign seat
common.QueueActioncommandType, commandParams (JSON), fireAtTick, predicateField?, predicateOp?, predicateValue?1Schedule a command (cancel is free, no refund)
common.CancelQueuedActionqueueId0Cancel a queued action

Notes

  • Hedge premium: A flat 2% premium is charged on all hedged volume (retailer SetHedgeFrac and genco SetFuelHedge).
  • Ramped levers set a target; the engine interpolates toward it over ~5 game-days. The commit dialog shows the trajectory.
  • Tenor levers accept a term in ticks (672 = 7 game-days, 35040 = 1 game-year). The commitment settles each tick during the term, then lapses.

Rate limits

LimitValueScope
API requests60/minPer seat bearer

Rate-limited responses return 429 with a Retry-After header (seconds).


Errors

All errors use the envelope { "error": "CODE", "message": "..." }.

HTTPCodeMeaning
400BAD_REQUESTMissing required field
401UNAUTHENTICATEDMissing/invalid token or API key
403FORBIDDENWrong scope or capability
404NOT_FOUNDResource not found
409IDEMPOTENCY_CONFLICTIdempotency key reused with different body
409SEAT_TAKENSeat was just claimed by another player
429RATE_LIMITEDRate limited
500INTERNALUnexpected server error

Business rejections return HTTP 200 with status: "rejected" -- they are not transport errors.

GRIDRUN