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):
- Log in at
gridrun.netwith your email (OTP code is sent to you). - Browse the lobby and join a seat -- pick a role and region.
- Request an API key from the dashboard. Keys require approval. Choose access level:
play(default) orfull(also join/vacate seats programmatically). - Reveal the key within 72 hours of approval (one-time window).
- 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:
| Header | Value |
|---|---|
X-API-Key | gr_live_... (your approved API key) |
Authorization | Bearer <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
| Command | Params | Description |
|---|---|---|
gov.SetCarbonTax | value ($/tCO2, 0-150) | Carbon price |
gov.SetRetailPriceCap | value ($/MWh, 0-500) | Retail ceiling |
gov.SetRenewableSubsidy | value ($/MWh, 0-100) | Clean generation subsidy |
gov.SetCapacityPayment | value ($/MW-day, 0-20) | Capacity market payment |
gov.SetImportTariff | value (0-0.5) | Import tariff fraction (0-50%) |
gov.SetInterventionStance | stance (active | passive) | Intervention mode |
gov.BuyStrategicReserve | coverageDays (1-30) | Buy reserve |
gov.Nationalise | targetSeatId | Take over a distressed seat (cash < 0, active stance) |
gov.Sanction | targetSeatId | Penalise a seat's standing (-30 in your region). Costs your governance score |
gov.LiftSanction | targetSeatId | Restore a sanctioned seat's access |
Retailer
| Command | Params | Description |
|---|---|---|
retailer.SetTariff | tariffType (flat | indexed | tou), flatPrice?, indexedMargin?, touPeakMult?, touOffPeakMult? | Tariff type + parameters |
retailer.SetHedgeFrac | value (0-1) | Forward hedge fraction |
retailer.SetSegmentOffset | segment (households | commercial | industrial | ev | municipal), pct (-0.15 to 0.15) | Per-segment price offset |
Genco
| Command | Params | Description |
|---|---|---|
genco.SetBidMarkup | value (0-0.5) | Bid markup (ramp) |
genco.SetForwardSell | frac (0-1), tenorTicks (672-35040) | Forward sell (tenor) |
genco.SetFuelHedge | frac (0-1) | Fuel hedge fraction |
genco.BuildPlant:ccgt | -- | Build CCGT (20 game-days) |
genco.BuildPlant:peaker | -- | Build peaker (7 game-days) |
Renewable
| Command | Params | Description |
|---|---|---|
renewable.SignPPA | volMW (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
| Command | Params | Description |
|---|---|---|
storage.SetDispatchPolicy | chargeBelow (0-2), dischargeAbove (0-3) | Arbitrage thresholds (x avg price) |
storage.SetAncillaryCommit | mw (0-200), tenorTicks (672-35040) | Ancillary commit (tenor) |
storage.BuildStorage | -- | Build battery (7 game-days) |
Fuel
| Command | Params | Description |
|---|---|---|
fuel.SetOfftake | frac (0-1), tenorTicks (672-35040, i.e. 7-365 game-days) | Offtake contract (tenor) |
fuel.SetStandingBudget | rate ($/game-day, 0-100000) | Standing budget (debited daily open) |
fuel.BuildInfrastructure | level (2-3, must = current+1) | Infrastructure upgrade (single-step) |
Materials
| Command | Params | Description |
|---|---|---|
materials.SetContractFrac | frac (0-1) | Contract fraction |
materials.SetDepletionRate | rate (1-20) | Extraction rate |
materials.SpendCommunity | amount ($, >=0) | Community spend (repair-only: raises popularity to 70 max, charges the effective delta) |
Manufacturer
| Command | Params | Description |
|---|---|---|
manufacturer.SetConversionCapacity | units (10-200) | Daily capacity |
manufacturer.SetEquipmentPrice | price ($50k-200k) | Equipment price |
manufacturer.Rewire | targetGen (1-8) | Retool production line |
Trading House
| Command | Params | Description |
|---|---|---|
trader.PlaceOrder | volume (100-10000) | Place a trade order (max 10 open positions) |
trader.SellForward | leg (fuel | power), volume (100-10000), tenorTicks (672-35040) | Forward contract (tenor) |
logistics.SetRedundancy | routeKey, spareMW (0-5000) | Route redundancy (increases queue with 3-game-day lead) |
Common
| Command | Params | Cost | Description |
|---|---|---|---|
common.SetMaintenanceBudget | assetId, rate (0-50000 $/tick) | 1 | Maintenance spend (max $50,000/tick per asset) |
common.EmergencyOverhaul | assetId | 1 | $1.5M instant repair |
common.ResignSeat | -- | 1 | Resign seat |
common.QueueAction | commandType, commandParams (JSON), fireAtTick, predicateField?, predicateOp?, predicateValue? | 1 | Schedule a command (cancel is free, no refund) |
common.CancelQueuedAction | queueId | 0 | Cancel a queued action |
Notes
- Hedge premium: A flat 2% premium is charged on all hedged volume (retailer
SetHedgeFracand gencoSetFuelHedge). - 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
| Limit | Value | Scope |
|---|---|---|
| API requests | 60/min | Per seat bearer |
Rate-limited responses return 429 with a Retry-After header (seconds).
Errors
All errors use the envelope { "error": "CODE", "message": "..." }.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Missing required field |
| 401 | UNAUTHENTICATED | Missing/invalid token or API key |
| 403 | FORBIDDEN | Wrong scope or capability |
| 404 | NOT_FOUND | Resource not found |
| 409 | IDEMPOTENCY_CONFLICT | Idempotency key reused with different body |
| 409 | SEAT_TAKEN | Seat was just claimed by another player |
| 429 | RATE_LIMITED | Rate limited |
| 500 | INTERNAL | Unexpected server error |
Business rejections return HTTP 200 with status: "rejected" -- they are not transport errors.