Back to overview

What Voltnir actually does.

01 · OVERVIEW

Voltnir is middleware that sits between your trading stack and the EPEX SPOT M7 intraday market. It owns the exchange connection, using AMQP for public/private feeds and WebSocket for market data, and handles everything that comes with it.

Internally, it maintains exchange state and exposes it to your systems over REST, WebSocket, and gRPC. It ships as a single static binary, developed in Rust for memory safety and speed, with no garbage collector pauses in the hot path.

This page explains, in detail, how each part of it works.

The page is written for three audiences. Traders will find how the order book is maintained and how position limits, cash limits, and real-time P&L are calculated. Risk and compliance will find how every order, trade, and account change is logged, attributed to a user, and retained for audit. Engineering and operations will find the transport contracts, the deployment model, and how the gateway behaves when a feed degrades or fails. Read it end to end, or jump to the section that matches your role.

02 · MARKET DATA

Before you can place an order you need a book to place it against. Voltnir owns the exchange connection and maintains that book for you: it applies each M7 update as the exchange publishes it and keeps the order book, the public trade tape, and the cross-border capacity matrix in memory, so every client reads a current, consistent picture without each one parsing the exchange feed. The same data is served three ways: WebSocket subscriptions, gRPC server-streams, and point-in-time REST reads.

Two upstream transports. Voltnir reaches M7 over a WebSocket connection and an AMQP connection, each carrying a public and a private side. The WebSocket runs over mutual TLS and carries the market_data and private_data streams; AMQP carries a public broadcast channel and a per-session private channel. Voltnir supervises every connection with automatic reconnect, exponential backoff, and primary/secondary host failover.

Snapshot, then deltas. On connect, M7 sends one full order-book snapshot per subscribed (delivery_area, product) key, a SynchronizationComplete marker, then incremental deltas. Voltnir holds the complete book in memory and recomputes top of book (best bid and best ask, with the cumulative size resting at that price) on every change. Clients receive the same model and derive whatever depth they need by sorting.

Sequenced, or resynced. Every snapshot and delta carries a sequence number that must be exactly one past the last, per (delivery_area, product). A gap or duplicate is treated as corruption: Voltnir drops the stream, marks the feed unsynchronised, and reconnects for a fresh snapshot rather than serve a torn book. Liveness is tracked by heartbeat (a missed-pong window), not by delta arrival, so a genuinely quiet market is never mistaken for a dead feed.

Contracts. A contract is one delivery period of a product in a delivery area, identified by (delivery_area, product, delivery_start) and addressable by its M7 contract id. Delivery areas are EIC codes; duration is carried verbatim as decimal hours (0.25, 0.5, 1.0). Voltnir stores only the delivery areas you configure. Once a contract's delivery end passes it is marked inactive and a delete is pushed to subscribers; an hour later it is tombstoned (its book cleared but its identity and trade history retained), so settled-contract P&L still resolves.

The public trade tape. Executed public prints are deduplicated by trade id and held in a rolling in-memory window of about twelve hours; on connect Voltnir backfills the last six hours so a fresh start is not blind. Each print carries price, quantity, execution time, the buy and sell delivery areas, a self-trade flag, and a state: ACTI is the active default state, while the recall and cancellation states (CNCL, RGRA, and related) mark a print the exchange has undone.

Cross-border capacity. The hub-to-hub feed is the XBID available-transfer-capacity (ATC) matrix between delivery areas: how many megawatts can still flow each way across each border. Capacity is passed through in M7's units (megawatts, and it can be negative), enriched with each border's current best bid and ask so the cross-zone spread reads in one feed, and resynced in full whenever its heartbeat drops. It is an opt-in feature; when disabled the matrix is simply empty.

Order book
The live book per contract, on the WS contracts stream: opt-in per delivery-area and product, a full snapshot on subscribe then upserts and deletes. REST GET /api/v1/contract/{area} and /{area}/{contract_id} for a point-in-time read; gRPC ListContracts / GetContract unary and WatchContract server-stream.
Public trades
The market's executed prints, on the WS public_trades stream: opt-in, live deltas with no snapshot. Seed a view with the one-shot public_trades_for_contract command (or REST GET /api/v1/public_trades, gRPC ListPublicTrades), then dedupe live updates by trade_id.
Trade tape
The same prints, pre-enriched with each trade's contract metadata, on the WS trade_tape stream: opt-in, with a 200-row chronological snapshot on subscribe. WebSocket-native; no REST or gRPC mirror.
Cross-border ATC
The XBID capacity matrix between delivery areas, on the opt-in WS watch_hub2hub command: a snapshot then per-border updates, each carrying both areas' best bid and ask. REST GET /api/v1/hub2hub, gRPC GetHub2Hub. Empty when the feature is disabled.

Authenticated, but unpermissioned. Reading market data needs a valid session and nothing more: no specific permission gates the book, the tape, or the capacity matrix on any transport, and none of it is exposed unauthenticated.

Capturing the feed. Market-data capture is off by default and configured under market_data as two independent streams: the public trade tape (public_trades) and the raw order-book frames (order_book, snapshots and deltas alike). Each opts in through its own persist setting, postgresql or parquet. A postgresql tape is written into and queried from the main audit database, which must itself be PostgreSQL or the gateway refuses to start; parquet writes export-only rotating files and works on any audit backend, SQLite included. Order-book capture is never queryable through the API; it is replay and backtest material only, written either to its own (or the audit database's inherited) PostgreSQL connection in daily partitions or to rotating Parquet files. Retention is per stream: the tape keeps one week by default, order-book capture keeps everything by default, and pruning only ever drops whole daily partitions or whole files, never row by row. Capture is fire-and-forget over a bounded channel: under overload it drops rows and logs rather than ever stall the live feed.

Feed health is a trading gate. The order-book feed's state (connected, synchronised, sequence-healthy, last-pong age) together with the measured per-delta processing time and end-to-end latency are exposed as rolling averages on GET /api/v1/state. The numbers are observed, not asserted. A new order is rejected with 503 when the feed is not green (§03), so an order is never sent against a stale or torn book.

03 · ORDER SUBMISSION

Every order, regardless of entry point, passes through the same validation pipeline before anything touches the wire. A REST POST /api/v1/order, a WebSocket new_order command, and a gRPC SubmitOrder call all funnel into the same fixed sequence of checks, executed in order, with no transport-specific shortcuts.

pre-flight order identity assigned on entry, global kill-switch and health gate checked before dispatch
  1. Idempotency key 400

    Each order carries a client_order_id. A caller-supplied value must parse as a UUID; if omitted, the gateway generates one. A supplied id already bound to a live (pending or acknowledged) order is rejected with 400. The duplicate check runs under the position-check lock alongside the insert, so two concurrent submits of the same id cannot both pass. The id is the order's handle for status, modify, and cancel.

  2. Kill-switch & health gate 422 / 503

    If the operator kill-switch is engaged (trade_enabled = false), a new order or a modify is rejected with 422; a cancel is exempt, so a resting order can always be pulled. Separately, if any of the exchange, AMQP, or order-book-feed health flags is not green, the order is rejected with 503. Neither sends an order into a halted or degraded session.

transport authenticate, validate the request, resolve the contract, normalise units
  1. Authentication & permission 403

    The caller is authenticated by API token and must hold the CreateOrder permission. The check is enforced at the gateway boundary; a caller without it receives 403 and the order is not processed.

  2. Request validation & contract resolution 400 / 404

    The request fields are validated before any shared state is read: the order-type, execution-restriction, and validity combination, delivery-area length, an iceberg display_qty below its quantity, a validity_date present when validity is good-till-date, and similar constraints. The target contract is then resolved, either from the contract_id supplied or from the (product, delivery_start) pair looked up against the live order book. A malformed field returns 400; an unresolved contract returns 404.

  3. Fixed-point units

    Price crosses the API as integer cents (5000 = €50.00/MWh) and quantity as integer thousandths of a MW (1000 = 1.0 MW). All position, cash, and fill arithmetic runs on these integers.

  4. Virtual-member resolution, or the global account 400

    If the order names a v_member_short_id, that virtual member must exist, be active, and be assigned to the calling user, unless the caller holds BypassMemberCheck. With no member named, the order trades the global house account, which requires the TradeGlobal permission; without it the order is rejected with 400.

position_check_lock one mutex, held from the first limit read until the order is pending
  1. The position-check lock

    A single mutex serialises the check-and-insert sequence. This closes the race where two simultaneously submitted orders each read the same pre-trade position, both pass the limit, and together breach it. The second submit blocks until the first is recorded as pending, then re-reads from the updated state. The lock is held from the initial limit read through to the pending insert.

  2. Two-sided position limit 422

    Exposure is bounded as a two-sided (max_short, max_long) pair rather than a single net figure, so an oversized buy cannot be masked by a resting sell. A buy widens max_long, a sell widens max_short. The order is rejected with 422 only when its side's bound is pushed past the configured limit and made worse than before, so an order that reduces an already-over-limit side still passes.

  3. Per-member position limit 422

    A member-tagged order is additionally checked against that virtual member's own max_position, and must clear both it and the account-wide limit. This bounds each virtual member independently under one EPEX membership, and returns 422 on breach.

  4. Cash limit 422

    Monetary exposure is also bounded: executed trades back to the 16:00-UTC working-day boundary, plus open orders including this one, must stay within the configured cash limit. EUR and GBP are separate pools with no FX conversion, so an order is checked only against its own currency's limit, and a money-making sell contributes zero rather than a negative. A member-tagged order is additionally checked against its member cash limit, which is capped at the global limit. The check runs only when a cash limit is configured, and returns 422 on breach.

  5. Self-trade prevention 422

    EPEX M7 does not block self-trades; it only flags them after execution. Voltnir checks before dispatch: if the order would cross one of the account's own resting or in-flight orders on the opposite side, the active policy applies. observe logs the cross and allows the order; reject blocks it with a SELF_CROSS_BLOCKED error (422).

  6. The pending insert

    Once the limit, cash, and self-trade checks pass and the client_order_id is confirmed unused, the order is inserted as PENDING and the lock is released. The pending order is now visible to the next submission's position read, which is what holds the serialisation. The order is tracked under its client_order_id before M7 has acknowledged it.

dispatch & ack
  1. Publish, then a bounded ack wait

    The order is encoded to the M7 wire format and published over AMQP. The call then blocks for a bounded window (2 seconds by default, configurable via system.order_ack_timeout_ms) for the exchange's first reply. A receipt acknowledgement confirms only that M7 has the message, not that the order is on the book; the order execution report that actually rests the order is a separate exchange message that can arrive later. Whichever lands first within the window decides the state the call returns, and that same blocking call and resulting state are returned on REST, WebSocket, and gRPC alike.

    • execution report rests it → ACTIVE or HIBERNATED201
    • receipt only, not yet rested → PENDING201
    • exchange refusal → REJECTED (+ reason)422
    • no answer in the window → TIMEOUT504
04 · ORDER LIFECYCLE

A submitted order is not static: M7 acknowledges it, rests it, may partially fill it, and finally fills or cancels it. Each order is identified by its client_order_id and carries a revision that increments on every change, so a client follows an order by querying it or by subscribing to the order stream. Placing, reading, modifying, and cancelling are separate, independently granted permissions, and every one of them is confined to the virtual members the caller is assigned to, reads and writes alike, enforced identically on REST, WebSocket, and gRPC.

Place
CreateOrder, plus assignment to the order's member (or TradeGlobal for the house account). REST POST /api/v1/order, WS new_order, gRPC SubmitOrder. Validation chain in §03.
Modify
ModifyOrder, plus authorization to act on the order's member. REST PUT /api/v1/order, WS modify_order, gRPC ModifyOrder.
Cancel
DeleteOrder, plus authorization to act on the order's member. REST DELETE /api/v1/order (one) and DELETE /api/v1/orders (all), WS delete_order, gRPC CancelOrder and CancelAllOrders.
Query / watch
ReadOrders for the firm-wide book, otherwise the caller's assigned members. Visibility only; it confers no right to act. REST GET /api/v1/order (one) and GET /api/v1/orders (list), WS orders stream, gRPC GetOrder, ListOrders, and WatchOrders.

Observing changes. An order's revision increments on every state change; fills and cancellations surface as it reaches a terminal state. A client reads the current state three ways: a one-shot query of a single order or the list, the always-on WebSocket orders stream, or the gRPC WatchOrders server-stream, which emits on add, change, and terminal state. The states are PENDING (sent to M7 but not yet rested by an order execution report), ACTIVE (resting and matchable), HIBERNATED (resting but not matching, such as a good-till-cancel order outside session hours), INACTIVE (fully filled or cancelled), and REJECTED. PENDING is a Voltnir-local state, not an M7 one: the order has been accepted by the gateway and published, and M7 may already have acknowledged receipt of the message, but the order stays PENDING until the execution report puts it on the book or a refusal rejects it.

Member isolation, on reads and on writes. By default a caller is confined to the virtual members assigned to it, and the confinement governs both seeing orders and acting on them. A read returns only orders tagged with an assigned member; untagged house-account orders are withheld. A modify or cancel is permitted only when the caller is assigned to the order's own member, with BypassMemberCheck acting across every member and TradeGlobal required for an untagged house order. The write check reads the order's existing member, never a value the caller supplies, so an order cannot be hijacked or re-tagged onto another desk. It is the same authorization that gates order placement, enforced again at every mutation.

Reading is not acting. ReadOrders grants visibility and nothing more: a read-only or compliance account can be given the firm-wide book, every member plus the house account, with no ability to place, modify, or cancel anything in it. The view grant and the act grant are independent and separately auditable, so oversight never implies authority.

A denied action is indistinguishable from a missing one. A modify or cancel the caller is not authorized for returns the same 404 not-found as a genuinely unknown id, never a 403. The mutation surface is therefore not an existence oracle: a desk cannot probe for, or enumerate, orders it may not act on. On the read path a cross-member filter is refused (REST 403, gRPC PermissionDenied, WS PermissionDenied), and the default scope is fail-closed: a session sees no orders until its scope is resolved.

Modifying. A modify targets a live order by id. A price or quantity change can grow exposure, so it re-runs the risk gates under the same position_check_lock as a new order: the two-sided position limit, the per-member limit, and the cash limit, each evaluated against the change in exposure, then the kill-switch and health gate. It runs neither the self-trade nor the idempotency check. A pure activate or deactivate leaves the net position unchanged and skips the risk gates. A second modify of the same order while the first is unconfirmed is refused with 409. The call returns 202 Accepted, and the new revision is confirmed asynchronously on the order stream.

Cancelling. A cancel only reduces exposure, so it runs no limit checks and is exempt from the kill-switch: a resting order can be pulled even while trading is halted. It confirms the order is live and not already mid-modify, refusing with 409 otherwise, then returns 202 Accepted; the order moves to INACTIVE once M7 confirms. Cancel-all respects the same isolation: a caller authorized across the whole account (BypassMemberCheck and TradeGlobal) gets the atomic exchange-side bulk cancel, while a member-scoped caller cancels only the orders it may act on, one per order, leaving other desks' and the house's orders untouched.

Attributable. Every placement, modify, and cancel is checked against the caller's permissions and member assignment at the gateway boundary and recorded with the acting user and virtual member in the audit trail (§09), queryable by user, member, and time.

05 · POSITION AND EXPOSURE

§03 walks the gates an order clears. This is what those gates measure: a net position you actually hold, and an exposure you could reach, bounded on both sides by a limit you set. P&L runs on the net; the limit checks run on the exposure.

short ◀── net = signed executed (ACTI) volume · span = net plus everything resting, both sides ──► long

Net versus exposure. Net position is your signed, executed-trade volume on a contract: the megawatts you are actually carrying, and the basis for realized P&L. Only trades in M7's active (ACTI) state count, so a cancelled trade backs out. Exposure adds every resting order, including an iceberg's hidden quantity, as a two-sided (max_short, max_long) pair, the worst case each way if everything resting fills. The position-limit check reads exposure, not net, so a limit is never breached by a fill you could already see coming.

Two-sided, with a carve-out. Each side is bounded independently against the configured limit. An order is refused only when it pushes the bound on the side it grows past the limit and makes it worse; an order that only reduces an over-limit side always passes. A limit of 0 therefore means reductions only, a hard freeze you can still trade out of.

Scoped. Every quantity is tracked per delivery-area/contract, per virtual member, and for the global house account. A member-tagged order must clear both its member's max_position and the account-wide limit.

Cash exposure, in money. Alongside megawatts, monetary exposure is bounded: executed trades back to the 16:00-UTC working-day boundary plus open orders, against a configured cash limit. EUR and GBP are separate pools with no FX between them, so each order is checked only against its own currency; GBP follows ECC's reservation methodology, and a money-making sell floors at zero. A member's cash limit is capped at the global one.

Operator-set, live. The position limit, the cash limit, the self-trade policy, and the trading kill-switch are all set by the operator and switchable at runtime over REST, gRPC, and WS, with no restart. They live in the gateway's profile store rather than static config, so a desk can tighten risk mid-session.

06 · P&L CALCULATION

P&L is derived, not stored. On every tick the gateway replays your filled trades and marks your open position against the live order book, producing realized and unrealized figures at four scopes. Each computation starts from scratch, so a restart loses nothing: M7 re-delivers the order snapshot and trade history on reconnect.

Realized: weighted-average cost. Your trades are replayed in execution order through a running position and a weighted-average open price. A trade on the same side as the position adds to that average; a trade on the opposite side closes against it and books closed × (close_price - avg_open_price), signed by whether you were long or short. A partial close leaves the average unchanged on the remainder; a larger opposite trade flips the position and re-opens the residual at the new price. Only trades in M7's active (ACTI) state count. Every trade is an execution; ACTI is its default state, and it drops out of P&L only if M7 cancels or recalls it (CNCL, RGRA, and the related recall and cancellation states). Open and pending orders never contribute, since an order is not an execution.

realized P&L · weighted-average cost · ACTI trades, execution order

  buy    5 MW  @ €100.00/MWh      long 5 MW,  avg open €100.00/MWh
  sell   5 MW  @ €110.00/MWh      closes 5 MW

  realized = closed × (close − avg_open)
           = 5 × (€110.00 − €100.00)  =  €50.00      position → flat

  long gains when close exceeds avg_open; short inverts.
  €50.00 = 5,000,000 q8  ·  hourly contract

Unrealized: marked to the book. The open position is marked at the contract's reference price: the mid of the best bid and ask when both sides of the book hold resting quantity, otherwise the last traded price, otherwise nothing (and unrealized is then zero). Every row reports which of the three produced its mark, so an unmarked position is explained rather than silent. Liveness is read from resting quantity on each side, not the price sign, because power contracts clear at zero and at negative prices and those marks are valid.

unrealized P&L · open position marked to the live book

  open   long 5 MW  @ avg €100.00/MWh
  mark   €110.00/MWh      mid = (best_bid + best_ask) / 2

  unrealized = position × (mark − avg_open)
             = 5 × (€110.00 − €100.00)  =  €50.00

  €50.00 = 5,000,000 q8  ·  hourly contract

Scopes and membership. Every figure is produced at four scopes: per contract, per (area, product), per virtual member per contract, and per (member, product). A member's position and realized P&L are computed from only that member's leg of each trade, independent of any other member on the same contract; an untagged trade rolls into the house account but into no member. Access is least-visibility by default: a caller sees only the members assigned to them, and the firm-wide per-contract and per-product books (which aggregate every member plus the house account) are withheld. The read_pnl permission (or bypass_member_check) returns the firm-wide snapshot. A v_member_short_id filter narrows to one member and is refused (REST 403, gRPC PermissionDenied, WS Forbidden) unless the caller is assigned to it or holds broad read; the same scope applies to the always-on P&L stream.

What counts, and the units. P&L counts filled trades only, so the displayed net position is trades-only and agrees with it; the position-limit check in §05 is the one place open orders are counted. On the wire, money is in q8 units (euro × 100,000, so divide by 100,000 for euro), price is €/MWh × 100, and position is MW × 1000. After a contract delists, realized P&L is preserved (the trades remain the source of truth) and unrealized drops to zero.

07 · TRANSPORT PARITY

Every unary operation is exposed on all three transports with identical fields, units, permissions, and error semantics. REST is JSON over HTTP/1.1; the WebSocket feed is one socket carrying both subscriptions and request/response commands; gRPC is protobuf over HTTP/2. Streaming exists only on WebSocket and gRPC.

REST:3000 WS:9001 gRPC:3443
orders submit / modify / cancel / query /api/v1 command unary RPC
contracts & market reads
system read / write limits, kill-switch, self-trade policy
users & members
streaming book, orders, trades, P&L, public none subscribe Watch*

unary operations are identical across all three transports; streaming has no REST equivalent.

Shared implementation. Each unary operation runs one code path regardless of transport, so fields, units, permissions, and error semantics do not diverge between REST, WebSocket, and gRPC. The three surfaces are asserted against each other at build time so they stay in step.

Versioning. REST is served under /api/v1; the WebSocket handshake accepts /ws/v1 (with / and /v1 as transition aliases, unknown versions rejected at upgrade); the gRPC service is voltnir.api.v1. The protocol version is echoed in the WebSocket config payload. Incompatible changes are published under a new version path; v1 is not mutated in place.

Transports. REST: JSON request/response on port 3000, usable from curl. WebSocket: port 9001, authenticate with {action:'auth', token} after connect, then issue subscriptions and commands on the same socket; stream frames are zstd-compressed binary, command responses are plain-text {type:'response', req_id, op, ok, result|error}. gRPC: service voltnir.api.v1 over HTTP/2 on port 3443, with an in-tree Python SDK generated from the service .proto.

Streaming. On WebSocket, the account streams (orders, trades, messages, status, P&L) are active once authenticated; contracts stream per delivery-area and product; public trades, the trade tape, and the hub-to-hub matrix are opt-in subscriptions. gRPC exposes the same data as server-streaming Watch* RPCs. There is no REST streaming surface. Market-data streams are covered in §02.

Encoding. Prices, quantities, and limits cross the wire as fixed-point integers: price in cents, quantity in thousandths of a MW. There are no floats in the API. API keys are SHA-256 hashed at rest and returned once at creation; the raw key is not stored and cannot be recovered.

08 · USERS, MEMBERS, AND 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. Twelve permissions compose the access model:

CreateOrder ModifyOrder DeleteOrder ToggleTrading SetPositionLimit ManageUsers ManageMembers ReadAudit ReadPnl 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), from read entitlements (ReadAudit for the audit log, ReadPnl for the firm-wide P&L), and from administrative ones (ManageUsers, ManageMembers). BypassMemberCheck is the deliberate exception that lets a privileged user act without virtual-member binding, and since it already lifts member restriction it also confers the firm-wide P&L read.

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.

09 · AUDIT AND PERSISTENCE

Orders and 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. The public trade tape can optionally be persisted on PostgreSQL and queried the same way, with a retention window you set.

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.
Order book capture
Optionally archive every raw M7 order-book frame, snapshots and deltas alike, off the feed hot path to PostgreSQL (daily-partitioned) or rotating Parquet files. Off by default; turn it on per deployment, with a configurable retention window. Your own captured book, for backtesting and market reconstruction.
What is never stored
Your strategy, model weights, forecasts and P&L logic never leave your box. The audit log records actions, not intent.
10 · TRADING TERMINAL

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.

11 · 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.
12 · LICENSING AND DEPLOYMENT

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.
Self-trade prevention
A pre-trade check that stops your own orders crossing each other: observe (flag) or reject (block before M7), switchable at runtime. EPEX doesn't prevent self-trades; Voltnir does.
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.
13 · SIMULATOR ACCESS

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.