Back to overview
// REFERENCE

What Voltnir actually does.

The landing page is the pitch. This is the mechanism. Below is what happens between your order and the exchange — the validation chain, the risk model, the full API surface, the terminal, and the operational story — in the detail a trading, risk, or engineering evaluator needs before they commit.

// 01 · OVERVIEW

A gateway between your stack and EPEX M7.

Voltnir is a Rust gateway that holds the EPEX SPOT M7 session for you — AMQP for order entry and private feeds, WebSocket for market data — and exposes it to your own software three ways: REST, a WebSocket push feed, and gRPC. It maintains the order books, session state, reconnects and gap detection; your strategy sends orders and reads the book.

Language
Rust, end to end. Single static binary, no JVM, no garbage collector in the hot path.
REST API
V1 JSON over HTTP — default port 3000.
WebSocket feed
zstd-compressed JSON frames — default port 9001.
gRPC API
Service voltnir.api.v1 over HTTP/2 — default port 3443.
Persistence
Pluggable: embedded SQLite (internal) or external PostgreSQL — selected in config, identical schema either way.
Bundled terminal
A React trading terminal the binary can serve itself — default port 8080.
Deployment
On your hardware. One outbound connection to EPEX M7; no phone-home.
// 02 · PLACING AN ORDER

What happens when you place an order.

A POST /api/v1/order (or the equivalent gRPC RPC) doesn't go straight to the wire. It runs a fixed validation chain first — every order, every transport, same order of checks. If any gate rejects, nothing reaches the exchange and the reason comes back to you.

  1. Authentication & permission

    The caller is authenticated by API token, and must hold the CreateOrder permission. No permission, no order — enforced at the gateway boundary, not in your client.

  2. Virtual-member resolution

    If the order names a v_member_short_id, that virtual member must exist, be active, and be assigned to the calling user. The short id is carried through to the exchange encoded in the M7 free-text field, so it can be recovered on the acknowledged order and on every fill. A user holding BypassMemberCheck is the only one who can skip member binding.

  3. Unit conversion to fixed-point integers

    Human units convert to integers before anything else touches them: price €/MWh → cents (×100), quantity MW → thousandths of a MW (×1000). There are no floats anywhere past this point — no rounding ghosts in limits, fills, or P&L.

  4. Position-check lock

    The read-modify-write of the position is taken under a lock keyed to the contract, so two orders submitted at the same instant can't each independently pass the limit and together breach it. The limit check sees a consistent before-state.

  5. Two-sided position-limit check

    Exposure is bounded on both sides independently — a (max_short, max_long) pair, not a single net number. The order's projected after-bounds are checked against the configured limit on the side it grows. You can't hide an oversized buy behind a resting sell: each side is capped on its own.

  6. Per-member limit

    A virtual member's own max_position is enforced separately from the account-wide limit. The order must clear both. This is what lets one EPEX membership host many entities without one of them spending another's headroom.

  7. Risk-reducing carve-out

    An order that only reduces exposure on an over-limit side is allowed even when you are already over the limit. You can always trade out of a position — the limit blocks growth, never an exit.

  8. Operational-health gate

    The order is rejected if trading is disabled by the operator kill-switch, or if the exchange / AMQP / market-data health flags aren't green. No order is sent into a degraded session.

  9. AMQP submit → client_order_id

    The order is encoded to the M7 wire shape and published over AMQP. You get a client_order_id back immediately and the order enters state PENDING — you can watch it from that id straight away.

  10. Bounded acknowledgement wait

    The gateway waits up to 10 seconds for M7 to acknowledge. On ack the order becomes ACTIVE; on a reject it becomes REJECTED with the exchange reason; if the window elapses with no answer, the timeout is surfaced rather than left hanging.

  11. State machine

    From there the order is driven by the exchange: PENDING → ACKNOWLEDGED → ACTIVE, with terminal states REJECTED, CANCELLED (inactive), and FILLED. Every transition is broadcast on the WebSocket and gRPC order streams.

REST  ·  submit
POST /api/v1/order
Authorization: Bearer ${VOLTNIR_TOKEN}
Content-Type: application/json

{
  "side":              "BUY",
  "price":             5000,            // €50.00 / MWh, as cents
  "quantity":          1000,            // 1.0 MW, as thousandths
  "delivery_area_id":  "10YNL----------L",
  "product":           "H",
  "delivery_start":    "2026-05-15T22:00:00Z",
  "v_member_short_id": "NL-BATTERY"
}

// → 201 Created
// {"client_order_id":"550e...","state":"PENDING","reason":null}
// → on M7 ack the order stream emits state ACTIVE
// 03 · RISK & POSITION MODEL

Two numbers, and they mean different things.

Voltnir tracks your net position (what you actually hold) separately from your exposure (what you could end up holding). The limit checks run on exposure; your P&L runs on the net position. Conflating the two is how desks get surprised.

Net position

Executed trades only — fully matched (ACTI) volume on a contract, signed by side. This is the megawatt position you are actually carrying, and the basis for realized P&L. Available per delivery-area/contract and per virtual member.

Exposure bounds

Executed trades plus your open resting orders (including the hidden quantity of icebergs), expressed as a two-sided (max_short, max_long) pair — the worst case each way if everything resting fills. This is what the position-limit check evaluates.

The carve-out, restated. Because the check is two-sided and compares the projected after-bounds to the before-bounds, an order that only shrinks the breaching side passes even when you're over the limit — and an order that grows it fails. A configured limit of 0 therefore means "no new exposure, reductions only": a hard freeze you can still trade out of.

Kill switches. Trading can be halted outright (trade_enabled = false), which fails the operational-health gate for every new order. Limits can be set per account and per virtual member. Together they're the off-switch the landing page promises — and they live on your box, under your control.

// 04 · API SURFACE

Three transports, one contract.

REST for request/response, WebSocket for a push feed, gRPC for streaming and production. The unary surface is mirrored exactly between REST and gRPC — same fields, same units, same permission gates, same error semantics — so moving from prototype to production is a transport swap, not a rewrite.

REST V1 — :3000

JSON over HTTP. Orders, modify/cancel, contracts, positions, P&L, audit, members and users, kill-switch. Documented endpoint-by-endpoint; prototype it in curl.

WebSocket — :9001

zstd-compressed JSON frames; authenticate with {action:'auth',token} after connect, then subscribe. Always-on streams (orders, trades, messages, status, P&L), per-area contracts, and opt-in public-trade and hub-to-hub feeds.

gRPC — :3443

Service voltnir.api.v1 over HTTP/2, protobuf-framed. Unary RPCs mirror REST one-for-one; server-streaming RPCs push book deltas, order events and fills. One .proto is the contract.

WebSocket streams. The push feed carries contracts, orders, trades, public_trades, trade_tape, messages, pnl, status, and state. Orders, trades, messages, status and P&L are on by default once authenticated; contracts are per area/product; public trades, the trade tape and the hub-to-hub matrix are opt-in.

Numbers behave like numbers. Every price, quantity and limit crosses the wire as a fixed-point integer — no floats anywhere in the API. Keys are hashed at rest. API keys are SHA-256 hashed and shown exactly once at creation; lose one, you rotate it. A first-party Python SDK wrapping the gRPC service ships in-tree, generated from the same .proto.

// 05 · MEMBERS, USERS & RBAC

Virtual members, named users, composable permissions.

One EPEX membership can host many virtual members — each with a short id, an active flag, and its own max_position. Users are assigned to members and hold a set of permissions, checked at the boundary on every action. Eleven permissions compose the access model:

CreateOrder ModifyOrder DeleteOrder ToggleTrading SetPositionLimit ManageUsers ManageMembers ReadAudit ExportReports RestartSystem BypassMemberCheck

A user with CreateOrder but not DeleteOrder can place orders and not cancel them — not their own, not anyone's. Trading entitlements (CreateOrder / ModifyOrder / DeleteOrder) are separate from operational ones (ToggleTrading, SetPositionLimit, RestartSystem) and from administrative ones (ManageUsers, ManageMembers). BypassMemberCheck is the deliberate exception that lets a privileged user act without virtual-member binding.

Attribution travels with every action: which user, which virtual member, which EPEX user_id, recovered even on the exchange acknowledgement because the member tag rides in the M7 free-text field.

// 06 · AUDIT & DATA

Every order and trade, stored on your box.

Orders, trades and public trades are persisted to your chosen backend and queryable through the REST audit endpoints — filter by user, member, event and time range; export as CSV or JSON.

Backends
Embedded SQLite or external PostgreSQL — same schema, same query results, chosen in config. Schema changes are mirrored across both connectors and verified by a dual-backend test battery.
Pagination
Cursor-based. limit defaults to 50, caps at 200; an explicit limit=0 is a 400, not a full-table scan.
Time basis
time_basis=delivery (default) filters by delivery overlap; time_basis=execution filters by when the action happened.
Export
CSV or JSON, gated by the ExportReports permission.
What is never stored
Your strategy, model weights, forecasts and P&L logic never leave your box. The audit log records actions, not intent.
// 07 · P&L

Realized and unrealized, broken down.

Voltnir computes profit and loss continuously and streams it on the always-on P&L feed, sliced the way a desk needs to read it.

Realized

Derived from executed trades against a weighted-average cost basis — the booked result of the volume you've actually matched and closed.

Unrealized

Your open net position marked against the live order book, so mark-to-market moves with the market rather than with your last fill.

Every figure is available per contract, per product, and per virtual member — the same attribution that runs through orders, audit and limits runs through P&L, so a brokerage or white-label desk can read each client's result in isolation.

// 08 · MARKET DATA

The book, the tape, and the neighbours.

Voltnir applies each M7 delta in roughly fifteen microseconds and maintains the books for you, so every client reads a current book without each one parsing the exchange feed.

Order book

Live depth, L1–L5, per contract — bids and asks with cumulative size, updated as the exchange publishes.

Public trade tape

The market's executed prints as they happen, plus a one-shot history backfill on subscribe, for VWAP and flow context.

Hub-to-hub ATC

The XBID cross-border available-transfer-capacity matrix between delivery areas, on an opt-in stream — the cross-zone picture in one feed.

// 09 · THE TRADING TERMINAL

A terminal you can trade on, on the same API.

Voltnir ships a React trading terminal built on the exact API any client uses — nothing privileged, nothing hidden. The binary can serve it directly on port 8080, or you can deploy it as a static SPA.

Chart & studies

Candlestick chart with order-flow and technical studies: VWAP, volume profile, point of control, cumulative delta, TWAP, opening range, absorption and divergence.

Trade & monitor

L1–L5 depth ladder, the public trade tape, order entry with pre-flight cash / position / throttle checks, live positions, audit, and P&L drill-down.

Admin & ergonomics

Members and users administration, the XBID hub-to-hub matrix, and eight built-in themes (plus one hidden) with adjustable density.

// 10 · CUSTOMER PORTAL

Accounts, licensing and downloads.

The customer portal is where you manage your account and self-serve everything Voltnir delivers outside the binary.

Accounts & licensing
Manage your account and the signed licence files bound to your EPEX SPOT user_id.
Downloads
The first-party Python SDK and the .proto contract for generating your own clients.
API docs viewer
The full REST, gRPC, configuration and deployment references, behind login.
Support
A direct line to us, scaled to your tier.
// 11 · SAFETY & OPERATIONS

Licensed, gated, and on your hardware.

Everything that keeps the gateway honest in production — and keeps you in control of it.

Licensing
Ed25519-signed licences, bound to an environment (SIM, PROD, or both) and verified by the binary at startup. Expiry is surfaced, not silently enforced: a 14-day grace period after the end date, with warnings at 30, 14, 7 and 1 days out.
Kill switches
Operator-toggled trading halt and per-account / per-member position limits — the off-switch lives with you.
Throttling
The gateway respects M7's order-management throttle (short- and long-window quotas) so you stay inside the exchange's rate envelope.
Health gating
Exchange, AMQP and market-data health flags gate order submission — no order enters a degraded session.
Deployment
A single static binary on your own Linux host, with an on-prem nginx per-port TLS reverse proxy in front of the gateway's plain HTTP/WS ports. One outbound connection to EPEX; the terminal can be embedded in the same binary.
// 12 · GET STARTED

See it on SIM.

Voltnir is free on the EPEX SPOT simulator. One static binary, one signed licence file, one outbound connection — minutes to deploy. Bring a Linux server and your EPEX SPOT credentials; we bring everything else.