Skip to content

Repository files navigation

mako ⚡

CI License Rust BDEW Container

⚠️ Experimental — Pre-1.0. APIs may change between releases. Not yet recommended for production without thorough in-house testing.

mako is the open-source market-operations platform for the German energy market: every regulated process — market communication, metering data, settlement, billing — modeled as a correct, auditable, event-sourced workflow, for every market role (NB, LF, MSB, ESA), from raw EDIFACT bytes to production microservices. In a sector of closed suites facing the IS-U sunset, mako is the only end-to-end platform whose source you can read, verify, and extend — built for the regulatory pace (LFW24, §14a, §41a, §42b/c, the EDIFACT→API transition) that batch-era systems strain under. The domain layer is deliberately split from transport and format so that when the market moves — MaBiS-Hub, the EDIFACT→API target landscape, European harmonization — mako moves with an import run, not a rewrite.

The workspace covers the full BDEW MaKo stack across five layers:

Layer What it is
Protocol edi-energy EDIFACT · dvgw-edi DVGW gas · redispatch-xml Redispatch 2.0 · mako-engine event-sourced process runtime · makod daemon
Market data mako-markt library · marktd Market Data Hub (PostgreSQL, durable CloudEvents fan-out, OIDC/JWT)
Settlement & billing grid-billing + netzbilanzd NNE/MMM/MSB settlement · eeg-billing + einsd EEG/KWKG · energy-billing + billingd retail billing
Customer management accountingd FI-CA ledger · portald customer portal · outputd customer documents · vertragd contracts · productd tariff catalog · agentd advisory automation
Agent surface 15 of the 17 services expose an MCP server — 163 tools — and agentd is the governed consumer: 28 specialists (26 read-only model-backed plus 2 coded) on agentplane, with journaled effects and durable human triage
Testing makotest — Python toolkit over the same Rust core: BDEW identifier check digits, the published answer-Frist table, the Entscheidungsbaum Antwortcodes, AHB-validated EDIFACT, counterparties that answer in EDIFACT, seeded EPEX curves, and a pytest plugin (README)

Architecture at a Glance

flowchart LR
    subgraph Market["Regulated market"]
        MP["Counterparty MSH<br/>(NB · LF · MSB · ÜNB)"]
    end

    subgraph Transport["Transport & process"]
        MAKOD["makod<br/>AS4 sign+encrypt · UNB…UNZ<br/>signed receipts · PID router"]
        PROCESSD["processd<br/>STP decisions"]
        MARKTD["marktd<br/>Market Data Hub"]
        SPERRD["sperrd<br/>Sperrung tracking"]
    end

    subgraph Settlement["Settlement & billing"]
        EDMD["edmd<br/>meter data · § 60 Abs. 1 MsbG"]
        NETZB["netzbilanzd<br/>NNE · MMM"]
        EINSD["einsd<br/>EEG/KWKG"]
        BILLINGD["billingd<br/>retail billing · risk gate"]
        INVOICD["invoicd<br/>INVOIC checking"]
        MABIS["mabis-syncd<br/>MaBiS 13003"]
        PRODUCTD["productd<br/>product catalog · EPEX"]
    end

    subgraph Business["Customer & operations"]
        VERTRAGD["vertragd<br/>contracts · §40b cadence<br/>§41e Aggregatorverträge"]
        ACCOUNTINGD["accountingd<br/>FI-CA ledger"]
        OUTPUTD["outputd<br/>customer documents<br/>+ delivery"]
        PORTALD["portald<br/>customer portal"]
        OBSD["obsd<br/>BNetzA KPIs"]
        AGENTD["agentd<br/>28 specialists<br/>26 model-backed + 2 coded"]
        ERP["ERP / operator systems"]
    end

    MP <-->|"AS4/ebMS3 · EDIFACT"| MAKOD
    MAKOD --> PROCESSD --> MARKTD
    MARKTD --> EDMD --> NETZB & EINSD & BILLINGD & MABIS
    MAKOD --> INVOICD
    PRODUCTD --> BILLINGD
    BILLINGD --> ACCOUNTINGD
    BILLINGD --> OUTPUTD
    VERTRAGD --> BILLINGD
    PROCESSD --> SPERRD
    EDMD & VERTRAGD --> PORTALD
    MAKOD -.->|"de.mako.*"| OBSD
    MAKOD & BILLINGD & EDMD -.->|"CloudEvents"| AGENTD
    AGENTD -.->|"de.agent.decision.made"| ERP
    ACCOUNTINGD --> ERP
Loading

Workspace at a Glance

Protocol & domain crates

Each crate's README carries its PID inventory, its Entscheidungsbäume and its regulatory sources.

Crate Purpose
edi-energy Parse · validate · build all 17 EDI@Energy EDIFACT message types
mako-engine Event-sourced runtime: Workflow, Process, EventStore, outbox, deadlines
mako-fristen The German market calendar — BDEW Werktage, the MaKo holiday table, the per-PID Antwortfristen
mako-markt Master data — MaloId, MeloId, MarktpartnerId, the BO4E gate, repository traits
mako-gpke GPKE Strom — Lieferantenwechsel, Ersatz-/Grundversorgung, Stammdatenänderung, Sperrung, PARTIN
mako-geli-gas GeLi Gas 3.0 — Lieferantenwechsel Gas, Stammdatenänderung, AWH Sperrprozesse
mako-wim Wechselprozesse im Messwesen, both Sparten — MSB-Wechsel, Geräteübernahme, Preisanfrage, INSRPT, ESA Wertebestellung
mako-mabis MaBiS Bilanzkreisabrechnung Strom — Summenzeitreihen, Clearinglisten, the MaBiS-ZP lifecycle
mako-gabi-gas GaBi Gas 2.1 — allocation, nomination, MMMA; typed GasDay / GasQuantity / GasBeschaffenheit
mako-emob NZR-EMob / Modell 2 — the virtual Bilanzierungsgebiet, its conservation identity and the three Modellwechsel legs
mako-redispatch Redispatch 2.0 workflows — §§ 13/13a/14 EnWG under BilAReM
mako-nbw Netzbetreiberwechsel (§ 46 EnWG) — the bulk handover of a whole Netzgebiet: PaketId, Aenderungszeitpunkt and the Fristenkalender of milestones. Has no PID or AHB of its own; it sequences Use-Cases the other crates own
mako-as4 BDEW AS4-Profil v1.2 over asx-rs — sign, encrypt, signed receipts, per-partner cert registry
dvgw-edi DVGW transport formats — ALOCAT, NOMINT, NOMRES, SSQNOT
redispatch-xml Redispatch 2.0 XML/XSD — all 9 document types
energy-api BDEW API-Webdienste Strom — REST/WebSocket client and Axum server

Settlement, billing & calculation crates

Crate Purpose
grid-billing Role-neutral grid settlement — NNE, MMM, MSB, AWH Gas, reversal and correction, each position carrying its legal-reference trace
eeg-billing EEG/KWKG feed-in settlement — 10 schemes, § 51 Negativpreisregel, § 52 sanctions, § 24 Anlagenerweiterung
energy-billing Retail billing (LF) — 13 product categories, § 41a dynamic tariffs, EN 16931 mapping
invoic-checker INVOIC plausibility — the eight-stage pipeline: Storno reference, period, Zahlungsziel, currency, arithmetic, total, Umsatzsteuer, tariff
mako-pruefung The BDEW Entscheidungsbäume, executable — the NB, LF, MSB, ESA, MaBiS and NZR-EMob answer rules, each behind its own role feature
mako-invoic The INVOIC settle/dispute state machine every billing family registers against

Platform crates

Shared by the daemons rather than by the domain. All 17 services link mako-service; 15 also link mako-events; mako-obs backs obsd alone.

Crate Purpose
mako-service The service SDK — run::<D>() over the Daemon / ServiceConfig traits, ServiceBuilder, config loading, health, OIDC, Cedar, MCP auth, the shared single-runner worker_lock
mako-events The CloudEvents type catalogue — every type string as a pub const, so producer/consumer drift is a compile error. Zero dependencies
mako-obs Observability projections and KPI computation — the domain types behind obsd, no I/O

Production services (17 daemons)

One line each; the maintained operator reference — routes, config, deployment — is mako docs · Services.

Service Port Role Purpose
makod :8080 · :4080 · :8090 All Protocol daemon — 71 workflows over 469 Prüfidentifikatoren, AS4 · REST · iMS
marktd :8180 All Market data hub — MaLo/MeLo, Versorgungsstatus, registries, durable CloudEvents fan-out
processd :8580 NB · LF · MSB Process decision engine — answers the published Entscheidungsbäume, escalates what it cannot decide
edmd :8380 All Energy data management — MSCONS, Zählerstandsgang, quality scoring, Ablesesteuerung, tiered store · 15 MCP tools
vertragd :9780 LF · MSB Contracts and customers — every contract with a Kunde on one side · 17 MCP tools
productd :9080 LF Product and tariff catalogue — 14 categories, Angebot lifecycle, EPEX and BEHG price series · 13 MCP tools
netzbilanzd :8680 NB Grid settlement runs — NNE, KA, MMM, MSB, AWH; issues the INVOIC · 8 MCP tools
einsd :9180 NB · LF Einspeiser registry and EEG/KWKG settlement · 19 MCP tools + 6 prompts
mabis-syncd :8880 ÜNB · NB MaBiS Summenzeitreihen — one filing per Bilanzierungsgebiet, with the clearing windows
invoicd :8280 LF INVOIC plausibility and the REMADV/COMDIS lifecycle · 7 MCP tools
billingd :9280 LF Retail billing — §§ 40, 40b EnWG, EN 16931, Abschlagspläne · 11 MCP tools
accountingd :9380 LF Massenkontokorrent — tamper-evident double-entry ledger, SEPA, Mahnwesen
outputd :9880 Customer communications — renders and delivers what other services computed
portald :9480 LF Customer portal read-model gateway and the § 41 EnWG self-service writes · 8 MCP tools
sperrd :8780 NB Sperr-/Entsperrauftrag execution queue · 4 MCP tools
obsd :8480 All Business-process observability — KPIs, Fristen, the § 7a Abs. 5 EnWG Gleichbehandlung report
agentd :9580 All Advisory agent plane — 28 specialist manifests over the platform's MCP tools, journaled and human-gated

✨ Features

EDIFACT layer (edi-energy)

Category Detail
📦 17 message types UTILMD, MSCONS, APERAK, CONTRL, INVOIC, REMADV, ORDERS, IFTSTA, INSRPT, REQOTE, PARTIN, ORDCHG, ORDRSP, QUOTES, COMDIS, PRICAT, UTILTS
🔍 Validation from the documents Nachrichtenstruktur, Segmentlayouts, formats and code lists of the MIG; the Prüfschablone of the AHB column with its Bedingungen; semantic cross-field rules — all appended to one ValidationReport, every finding naming its place by the MIG's Nr
🔤 Declared character repertoire UNB+UNOC:3 is ISO 8859-1, not UTF-8 — parsing transcodes by the repertoire the interchange itself declares, and InterchangeBuilder encodes back into it
📅 Annual release lifecycle Multi-version registry keyed on the Anwendungszeitpunkt. No grace window — Allgemeine Festlegungen 6.1 § 2.5 gives each format one cut-over instant (1 April / 1 October), so DEFAULT_RECEIVE_TOLERANCE_DAYS is 0
🔒 Security by default DoS limits (max 10 MB, 10 000 segments), log-injection sanitisation, eight cargo-fuzz targets over the untrusted-input boundary — compiled on every push, run weekly
🛠️ Fluent message builders Type-state builder API with compile-time mandatory field enforcement
🔁 Round-trip serialisation Parse → validate → serialize with byte-exact EDIFACT output
🧪 Profiles read from the BDEW PDFs 32 profiles across 17 types, imported by cargo xtask import-profiles from the MIG and AHB documents — every Anwendungsfall's skeleton validates against its own Prüfschablone

DVGW gas transport layer (dvgw-edi)

Category Detail
📦 4 DVGW message types ALOCAT, NOMINT, NOMRES, SSQNOT — identified by BGM DE 1001, since every one rides ORDERS/ORDRSP
🔗 Published Zuordnung correlation_key() applies the Zuordnungstupel the Nachrichtenbeschreibungen assign per Prüfidentifikator (ZO-T1ZG-T1, SSQNOT's Netzkonto tuple); process_key() adds the gas day or Abrechnungszeitraum
🔢 Real Prüfidentifikatoren SG1 RFF+Z13 carries DVGW's own 70001–70096; dvgw_edi::catalogue() ships every published Anwendungsfall with its direction
⚖️ Energy from the unit a QTY in KW1/KW2 is a rate integrated over its DTM+2; KWH is energy — totals stay per direction
🧪 Independent of edi-energy Separate DvgwPlatform; sniff() tells the two families apart at the ingest boundary
📜 Regulatory basis BNetzA BK7-24-01-008 · Kooperationsvereinbarung Gas · DVGW-Nachrichtenbeschreibungen ALOCAT 5.11a, NOMINT 4.6, NOMRES 4.7, SSQNOT 5.7

Redispatch 2.0 XML layer (redispatch-xml)

Category Detail
📦 9 CIM/IEC 62325 document types ActivationDocument, PlannedResourceSchedule, AcknowledgementDocument, Stammdaten, Unavailability, NetworkConstraintDocument, Kaskade, StatusRequest, Kostenblatt
🔍 Two-phase validation parse_and_validate() — XSD structural check + semantic cross-field rules in one call
🔁 Round-trip serialization Parse → serialize with byte-stable XML output
🔑 Document correlation Document::mrid(), sender_id(), receiver_id() — routing keys for AcknowledgementDocument process matching
🔒 #![deny(unsafe_code)] Memory-safe XML processing; no unsafe in the parse path
📜 Regulatory basis BNetzA BK6-23-241 · BK6-20-059 TZ 2 · §§ 13, 13a, 14 EnWG

Master data layer (mako-markt)

Category Detail
🆔 Validated domain IDs MaloId (11-digit BDEW check-digit), MeloId (DE+31-char), MarktpartnerId (13-digit; auto-derives NAD DE3055 agency code 293/332/9 from prefix)
🗂️ 29 repository traits One trait per aggregate — MaloRepository, MeloRepository, NbContractRepository, PartnerRepository, LokationszuordnungRepository, TechnischeRessourceRepository, SteuerbareRessourceRepository, CorrelationIndex, … — AFIT, no dyn Trait overhead
Temporal role assignments Rollenzuordnung with valid_from/valid_to — evaluated against CET/CEST German calendar date at query time
📨 CloudEvents 1.0 Outbound events (MarktEvent) with HMAC-SHA256 signing; InboundMakoEvent for receiving makod lifecycle events
🧪 testing feature Two in-memory doubles only — InMemoryVersorgungsStatusRepository (§§ 36/38 EnWG) and InMemoryNbEnergiemixRepository (§ 42 EnWG). A double is a second implementation that drifts, so every other repository is tested against real PostgreSQL
🚫 Zero framework deps No axum, sqlx, or async runtime — pure domain library; all I/O lives in services/marktd

BO4E typed API (marktd)

94 active rubo4e::current types — every payload, in or out, crosses one four-stage gate, and a BO4E document in a request body is a Bo4e<T>, so the gate runs while serde deserialises and no handler can forget it.

Category Detail
📦 Typed responses GET /api/v1/malosMarktlokation; GET /api/v1/melos/{melo_id}Messlokation; GET /api/v1/melos/{melo_id}/zaehlerZaehler; GET /api/v1/zaehler/{zaehler_id}/geraeteGeraet — all canonical BO4E camelCase. Only MaLos have a collection route; the rest are addressed per id or nested under their parent
🔍 One gate on write mako_markt::bo4e::decode at every endpoint: _typ → typed deserialization → strict enums by JSON-path → the rules BO4E states in prose and enforces nowhere. Every refusal is a 422 with the same code
🧬 The gate is in the type A BO4E field in a request body is a Bo4e<T>, which runs all four stages inside Deserialize — there is no constructor that skips them. A serde_json::Value documented as a BO4E payload is refused by cargo xtask check-request-bodies, with an empty exemption list
🧩 Lokationsbündel, audited against the BDEW codelist EDI@Energy's Codeliste der Lokationsbündelstrukturen — 15 structures, 27 object codes — decides which structure a bundle declares and where each object sits in it. An object code standing for another object type is a 422
🚧 Bodies refuse what they cannot store Every Json<T> request body denies unknown fields, so a caller naming a field the API does not have gets a 422 naming it rather than a 2xx and a value that went nowhere
📤 Nothing is emitted that would be refused The same rules run outbound — over every shape the three billing engines can produce, and at runtime wherever a document is assembled (a Sammelrechnung, a Rechnung merged with its Fremdkosten). Money is compared at the scale of the stated total
🏦 Identifiers and bank details A customer's IBAN (ISO 7064 MOD-97-10) and BIC (ISO 9362) are checked before storage, so a typo is a 422 rather than a returned direct debit; MaloId, MeloId and EicCode carry their check digits
📋 Vertrag for LRV exchange nb_contracts stores full BO4E Vertrag JSONB + typed SQL columns; PUT /api/v1/nb-contracts validates vertragsart / vertragsstatus; emits de.markt.nb-contract.updated CloudEvent
👤 Geschaeftspartner typed partners PUT /api/v1/partners/{mp_id} puts the BO4E Geschaeftspartner through the gate and stores the canonical round-trip. GET returns the typed geschaeftspartner field.
🔢 Zaehlwerk register access GET /api/v1/zaehler/{id}/zaehlwerkeVec<Zaehlwerk> — OBIS registers for TOU billing and iMSyS demand management
ZaehlzeitRegister + ZaehlzeitSaison GET/PUT /api/v1/zaehler/{id}/register + /zaehler-register/{id}/saisons — iMSys TOU register definitions (HT/NT/EINZEL); GET /api/v1/zaehler/{id}/tariff-zone?datetime=ISO resolves zone in one SQL JOIN (§14a Modul 2)
Energiemenge deliveries edmd serves GET /api/v1/deliveries/{malo_id} → a BO4E Energiemenge array — typed ERP-consumable meter readings without EDIFACT parsing (alongside Lastgang and Zeitreihe on the same store)
💰 MMMA settlement prices GET/PUT /api/v1/mmma-preise/gas/{year}/{month} — Gas MMM Abrechnungspreise (Trading Hub Europe); GET/PUT /api/v1/mmm-preise/strom/{year}/{month} — Strom MMM Ausgleichsenergie per ÜNB. Both auto-fetched by netzbilanzd and validated by invoicd check 6.
🗂️ Fallgruppe + Bilanzierungsmethode auto-extract makod adapters extract bilanzierungsmethode (Z01→SLP, Z02→RLM, Z04→IMS) and fallgruppe (GaBi Gas, TM+Z10) from UTILMD TM+EM / TM+Z10 segments. marktd event_ingest calls patch_typenmerkmal() on de.mako.process.initiated (PIDs 55001/44001) to keep malo.fallgruppe / malo.bilanzierungsmethode in sync.
🏷️ Tarifpreisblatt + Preisblatt productd stores all energy products as Tarifpreisblatt JSONB; category drives calculator selection; all prices are user-defined; schema validated on PUT (wrong _typ → 422); queried by billingd calculator for pricing inputs
🔒 One vocabulary per column Typed columns are derived from the typed BO, never a string lookup on its JSON, and hold BO4E wire values only. Each enum column's SQL CHECK is that enum's VARIANTS, compared against the schema by a mako-markt test.
🧭 UTILMD characteristics read by class makod reads SG10 CCI/CAV by DE 7059 Klassentyp and DE 7037 Merkmal, because the code spaces overlap (Z18 = Regelzone or „Kein Haushaltskunde"), and maps them to BO4E enums — each citing its MIG Strom S2.2 / Gas G1.2 segment number
🏷️ Namespaced BO4E extensions What BO4E does not model rides in a ZusatzAttribut named mako:<snake_case> — 42, each registered with what it carries. BO4E mandates no convention for its extension slot; mako is a prefix rubo4e itself registers, and cargo xtask check-bo4e-attributes reads it from there
Outbound BO4E conformance Every emission site crosses the same gate, because an engine test covers the shapes a builder produces but not the values a request supplies. Out-of-schema fields are refused alongside values; documents are built typed, never assembled as JSON
🧾 Steuerbetrag + Registeranzahl energy-billing projects the EN 16931 BG-23 tax breakdown into BO4E Steuerbetrag entries on the Rechnung JSON; Registeranzahl (Eintarif/Zweitarif) drives HT/NT position branching
🏦 Zahlungsinformation + Zahlungsart accountingd SEPA mandate registry stores structured payment info; pain.008 XML generated from SepaMandateRow (IBAN, BIC, Kontoinhaber, Mandatsreferenz)

Process engine layer (mako-engine + domain crates)

Category Detail
♻️ Event-sourced processes Optimistic-concurrency event append with SlateDB-backed storage
⚛️ Atomic dual-write Events and outbox messages written in a single WriteBatch via AtomicAppend
Regulatory deadlines DeadlineStore over the windows mako-fristen publishes per Prüfidentifikator — GPKE clock times on the 1. Werktag (11:00/06:00/05:00), WiM Strom 3/5/7/1 Werktage, GeLi Gas 4/3/2 Werktage. Never a flat 24 h, and the GeLi Gas „10 Werktage“ is the supplier’s Vorlauffrist, not an answer window
📨 AS4 inbound transport makod receives BDEW AS4 pushes via asx-rs, deduplicates with SlateDbInboxStore, routes by Pruefidentifikator
🔐 Cedar ABAC authorization All HTTP endpoints gated by Cedar attribute-based access control; built-in default policy with custom policy overlay via --cedar-policy-dir
🪪 OIDC / JWT + API-key auth JWT bearer tokens from Azure AD, Keycloak, Okta, Kubernetes workload identity; RS256/ES256/PS256 families only; JWKS cached with background refresh; coexists with named API keys
📡 CloudEvents 1.0 ERP webhooks Outbound ERP notifications as CloudEvents 1.0 structured-mode JSON (application/cloudevents+json), HMAC-SHA256 signed; natively routable by SAP BTP, AWS EventBridge, Azure Event Grid, Google Eventarc
🔄 Format-version coexistence Processes started under FV2025-10-01 run to completion under those rules even after FV2026-10-01 cutover
🪦 Dead-letter sink Structured DeadLetterReason variants — UnknownPid, DuplicateMessage, VersionMismatch, …

🚀 Quick Start — run a demo

Three runnable stacks under demos/, each with a docker compose file and a smoke.sh that asserts every step.

Demo Services What it proves
demos/nb-stp makod · marktd · processd A UTILMD 55001 Anmeldung arrives over the EDIFACT door, mako-pruefung walks E_0622, and the 55002 Bestätigung goes back — automatically, inside the Frist
demos/eeg-billing marktd · edmd · einsd A month of quarter-hour Einspeisemengen settles into a § 21 EEG 2023 Vergütung and a § 14 Abs. 2 UStG Gutschrift
demos/o2c productd · vertragd · billingd · outputd · accountingd The retail money path: a Tarifpreisblatt prices a Vertrag, the invoice becomes a stored document and an Offener Posten, and a payment closes it
just build-demo                 # makod, marktd, processd
cd demos/nb-stp     && docker compose up -d && bash smoke.sh

just build-demo-eeg             # marktd, edmd, einsd
cd demos/eeg-billing && docker compose up -d && bash smoke.sh

just build-demo-o2c             # productd, vertragd, billingd, outputd, accountingd
cd demos/o2c        && docker compose up -d && bash smoke.sh

just build-demo is the quick one: its three images come out of a demo-builder stage that compiles only what the Lieferbeginn path needs. The other two build every service in the workspace in one cargo build, so budget 20–45 minutes on a cold cache and a few minutes on a warm one.

The Getting Started guide walks the first one step by step.


🚀 Quick Start — EDIFACT parsing

cargo add edi-energy
use edi_energy::{parse, EdiEnergyMessage};

let input = std::fs::read("Netznutzung_20241015.edi")?;
let msg = parse(&input)?;
let report = msg.validate()?;
println!("Valid: {}", report.is_valid());

🚀 Quick Start — Process engine

cargo add mako-engine --features testing
cargo add mako-gpke
use mako_engine::{
    builder::EngineBuilder,
    ids::TenantId,
    version::WorkflowId,
    event_store::InMemoryEventStore,
};
use mako_gpke::lf_anmeldung::GpkeLfAnmeldungWorkflow;

let ctx = EngineBuilder::new()
    .with_event_store(InMemoryEventStore::new())
    .build();

// Spawn a new process for one delivery point.
let process   = ctx.spawn::<GpkeLfAnmeldungWorkflow>(TenantId::new(), wf_id);
let envelopes = process.execute(initiate_cmd).await?;

// Reconstruct typed state by replaying all persisted events.
let state = process.state().await?;

🚀 Quick Start — DVGW gas transport

cargo add dvgw-edi
use dvgw_edi::{DvgwMessageType, DvgwPlatform};

// Identity comes from BGM DE 1001 — UNH only names the ORDERS/ORDRSP carrier.
let msg = DvgwPlatform::default().parse(edi_bytes)?;
println!("{} ({})", msg.message_type, msg.document.description());

// The gas day is DTM+Z01; every quantity carries its own DTM+2 period.
if let Some(gas_day) = msg.gas_day() {
    println!("Gastag {gas_day}");
}
// Energy per direction: rates (KW1/KW2) integrated over their period, KWH as is.
for (qualifier, kwh) in msg.energy_by_qualifier() {
    println!("{qualifier}: {kwh} kWh");
}
// The published Zuordnungstupel, ready for a process registry.
println!("{:?}", msg.process_key());

if msg.message_type == DvgwMessageType::Ssqnot {
    let record = dvgw_edi::ssqnot::MehrMindermengenmeldung::from_message(&msg)?;
    println!("Netzkonto {} Saldo {} kWh", record.netzkonto, record.saldo_kwh());
}

let report = DvgwPlatform::validate_message(&msg);
for issue in report.errors() {
    eprintln!("{issue}");
}

🚀 Quick Start — Redispatch 2.0 XML

cargo add redispatch-xml
use redispatch_xml::{parse_and_validate, serialize, detect, DocumentType};

// Optionally detect document type before parsing (useful for routing)
let doc_type = detect(xml_bytes);

// Parse + validate in one step (recommended)
let doc = parse_and_validate(xml_bytes)?;

// Primary routing keys — use to correlate AcknowledgementDocument to process
println!("mRID:     {}", doc.mrid());
println!("sender:   {}", doc.sender_id());   // EIC of TSO/RSO
println!("receiver: {}", doc.receiver_id());

// Serialize back to XML (byte-stable round-trip)
let out = serialize(&doc)?;

🚀 Quick Start — Master data (mako-markt)

cargo add mako-markt --features testing
use mako_markt::domain::{MaloId, MeloId, MarktpartnerId};

// Validated identifiers — construction returns Err on malformed input
let malo_id = MaloId::new("51238696012")?;
let melo_id = MeloId::new("DE0001234567890123456789012345678")?;
let mp_id   = "9900357000004".parse::<MarktpartnerId>()?;

// NAD DE3055 agency code derived from MP-ID prefix automatically:
// "99…" → "293" (BDEW Strom), "98…" → "332" (DVGW Gas), other → "9" (GS1)
assert_eq!(mako_markt::domain::nad_agency_code(&mp_id), "293");

// In tests — two in-memory doubles, for the traits whose *regulatory* state
// machine is worth asserting without a database. Every other repository is
// exercised against real PostgreSQL in marktd's integration suites.
use mako_markt::testing::InMemoryVersorgungsStatusRepository;
let repo = InMemoryVersorgungsStatusRepository::default();

📋 Format and Document Coverage

BDEW EDI@Energy (edi-energy) — 17 EDIFACT message types

Message EDIFACT type Latest release Use case
UTILMD Strom UTILMD S2.2 (fv20261001) Grid connection (supplier switch, registration)
UTILMD Gas UTILMD G1.2 (fv20261001_gas) Gas grid connection processes
MSCONS MSCONS 2.5 (fv20261001) Metered services consumption reports
APERAK APERAK 2.2 (fv20261001) Application error acknowledgements
CONTRL CONTRL 2.0b (fv20260101) Interchange control acknowledgements
INVOIC INVOIC 2.8e (fv20261001) Invoices
REMADV REMADV 2.9e (fv20260401) Remittance advice
ORDERS ORDERS 1.4c (fv20261001) Purchase orders
IFTSTA IFTSTA 2.1 (fv20261001) Multimodal status reports
INSRPT INSRPT 1.1a (fv20260101) Inspection reports
REQOTE REQOTE 1.3c (fv20261001) Requests for quotation
PARTIN PARTIN 1.1 (fv20261001) Party information
ORDCHG ORDCHG 1.2 (fv20261001) Purchase order changes
ORDRSP ORDRSP 1.4c (fv20261001) Purchase order responses
QUOTES QUOTES 1.3c (fv20261001) Quotations
COMDIS COMDIS 1.0g (fv20260401) Commercial dispute (Handelsunstimmigkeit)
PRICAT PRICAT 2.1 (fv20261001) Price/sales catalogue
UTILTS UTILTS 1.1e (fv20261001) Technical master data

DVGW gas transport (dvgw-edi) — 4 message types

Message Version Carrier · BGM DE 1001 Direction Use case
ALOCAT 5.11a ORDRSP · X1GXBG NB → MGV, MGV → BKV, ENB/ANB → NB, MGV → NB, NB → BKV Allokation, Mengenmeldung NKP, Clearing
NOMINT 4.6 FK ORDERS · 01G 55G Y1G Y6G Y7G Transportkunde → NB/MGV Nominierung, Re-Nominierung (RFF+AGO + DTM+9)
NOMRES 4.7 FK ORDRSP · 07G 08G 19G 20G Y2G NB/MGV → Transportkunde Matching-Benachrichtigung, Bestätigung
SSQNOT 5.7 FK ORDRSP · BAG NB → MGV Mehr-/Mindermengenmeldung zur Führung des Netzkontos

The other DVGW transport formats (SCHEDL, IMBNOT, TRANOT, DELORD/DELRES, CHACAP, NUEVOR, SLPASP, TSIMSG) are not parsed, and a workflow for a format nothing parses would be unreachable.

Redispatch 2.0 XML (redispatch-xml) — 9 document types

BK6-23-241 (07.05.2026) is the basis, and it repealed its predecessors. BK6-20-060 and BK6-20-061 are gone (Tenorziffern 4 and 3), BK6-20-059 Tenorziffer 1 with the end of 30.06.2026 — and what replaces them is not a new table of Fristen but an obligation on the ÜNB to develop bundesweit einheitliche Prozessbeschreibungen (Tenorziffer 7). So a deadline here is either sourced from a document that still states it, or the operator's own, with the historical figure offered as a labelled default (fristen::Betreiberfristen).

Document type Deadline Where it comes from
AcknowledgementDocument 3 min from receipt of the Übertragungsdatei sourcedAcknowledgementDocument FB 1.0g. Never six hours
ActivationDocument 5 min operator's own; historically BK6-20-060 (repealed)
Stammdaten (VNB → ÜNB) 1 Werktag operator's own; historically BK6-20-060 (repealed)
Kostenblatt 15th of the following month operator's own; historically BK6-20-061 (repealed)
PlannedResourceScheduleDocument Vorab-Information 30 min before validity (Prognosemodell) sourced — BilAReM Kap. 6.3.1
Unavailability_MarketDocument
NetworkConstraintDocument
Kaskade
StatusRequest_MarketDocument none it is a Marktpartner availability notification, not a request/response pair — there is no answer document and no 24-hour window

📖 Documentation

Full documentation lives at hupe1980.github.io/mako — a searchable site (source under site/, built with Zola).

Section What's inside
Guide Install, parse your first interchange, run a workflow
Architecture Event-sourced engine, domain model, deadlines, ERP/API integration
Reference Parsing, validation, builders, the platform API, the full process catalog, AS4, DVGW, Redispatch
Services Operator guides for all 17 daemons — ports, config, APIs, deployment
Regulatory BNetzA determinations and the authoritative Prüfidentifikator catalog
Release & Compliance Annual EDI@Energy release lifecycle, schema versioning, license governance
API Reference (docs.rs) Full rustdoc for the published crates

💡 Usage Examples

Parse a single message

use edi_energy::{parse, AnyMessage, EdiEnergyMessage};

let msg = parse(bytes)?;

match &msg {
    AnyMessage::Utilmd(m) => {
        println!("PID: {}", m.detect_pruefidentifikator()?.as_u32());
        if let Some(bgm) = m.bgm() {
            println!("Doc code: {}", bgm.document_code);
        }
    }
    AnyMessage::Mscons(m) => {
        println!("Consumption report, {} segments", m.segments().len());
    }
    AnyMessage::Unknown { message_type_code, .. } => {
        println!("Unrecognised type: {message_type_code}");
    }
    _ => {}
}

Validate and inspect issues

use edi_energy::{parse, EdiEnergyMessage};

let msg = parse(bytes)?;
let report = msg.validate()?;

if !report.is_valid() {
    for issue in report.errors() {
        println!(
            "[{}] {} — {}",
            issue.rule_id.as_deref().unwrap_or("-"),
            issue.segment_tag.as_deref().unwrap_or("-"),
            issue.message,
        );
    }
}
report.into_error_result()?;

Parse a multi-message interchange

use std::io::Cursor;
use edi_energy::{parse_interchange, EdiEnergyMessage};

let reader = Cursor::new(bytes);
for msg_result in parse_interchange(reader) {
    let msg = msg_result?;
    if let Some(mt) = msg.try_message_type() {
        println!("{} — PID {:?}", mt.as_str(), msg.detect_pruefidentifikator().ok());
    }
}

Build a UTILMD message

use edi_energy::{builders::UtilmdBuilder, Pruefidentifikator, releases};

let bytes = UtilmdBuilder::new(releases::utilmd_fv20261001().clone())
    .pruefidentifikator(Pruefidentifikator::new(55001)?)
    .sender("4012345000023")
    .receiver("9900357000004")
    .document_code("E01")
    .document_date("20261001")
    // `IDE+24` DE 7402 carries a Vorgangsnummer, never a Lokations-ID.
    .transaction("VORGANG-0001")
        // `SG4 DTM+92` — the Lieferbeginn.
        .date("92", "20261101")
        // `SG5 LOC+Z16` names the Marktlokation the Vorgang is about.
        .marktlokation("51238696012")
        .done()
    .serialize()?;

🏗️ Architecture

mako/
├── crates/
│   ├── edi-energy/          # EDIFACT parse · validate · build · serialize
│   │   ├── src/             # EdiEnergyMessage, Platform, builders, registry
│   │   └── profiles/        # BDEW JSON profile data (MIG + AHB + codelists)
│   │
│   ├── mako-engine/         # Event-sourced process runtime
│   │   └── src/             # Workflow, Process, EngineBuilder, all store traits
│   │                        # + SlateDB implementations, fristen, dead-letter
│   │
│   ├── mako-gpke/           # GPKE Strom — Lieferantenwechsel, Ersatz-/Grundversorgung,
│                            # Stammdatenänderung, Sperrung, Meldepflichten, PARTIN, INVOIC
│   ├── mako-wim/            # WiM Strom + Gas — MSB-Wechsel, Geräteübernahme, Preisanfrage,
│                            # INSRPT, ESA Wertebestellung and the UC 4.5 billing round trip
│   ├── mako-geli-gas/       # GeLi Gas 3.0 — Lieferantenwechsel Gas, Stammdatenänderung,
│                            # AWH Sperrprozesse, PARTIN Gas, INVOIC
│   ├── mako-mabis/          # MaBiS Strom — six workflows: Bilanzkreisabrechnung, Profile,
│                            # Clearingliste, MaBiS-ZP lifecycle, Anforderung, Listenabgleich
│   ├── mako-emob/           # NZR-EMob / Modell 2 — virtual Bilanzierungsgebiet, allocation engine
│   │                        # (Anlage 6 §IV.1 conservation identity, ¼-h session split, BG lifecycle)
│   ├── mako-gabi-gas/       # GaBi Gas 2.1 — INVOIC 31007/31008/31010 + MSCONS 13013 + DVGW ALOCAT/NOMINT/NOMRES; typed domain: GasDay/GasQuantity/GasBeschaffenheit/AllocationVersion
│   ├── mako-as4/            # BDEW AS4-Profil v1.2: BdewAs4Profile, bdew_pmode (ECDSA+ECDH-ES, BrainpoolP256r1)
│   │                        # bdew_push_policy (require_encrypted_inbound), BdewTestPki, MockAs4Endpoint
│   ├── dvgw-edi/            # DVGW EDIFACT formats — ALOCAT, NOMINT, NOMRES, SSQNOT
│   ├── energy-api/          # BDEW REST/WebSocket API client + Axum server (iMS)
│   ├── mako-redispatch/     # Redispatch 2.0 process engine — 8 XML-document-driven workflows
│   ├── mako-nbw/            # Netzbetreiberwechsel (§ 46 EnWG) — Paket-ID, Änderungszeitpunkt,
│                            # Fristenkalender; sequences other crates' Use-Cases, owns no PID
│   ├── redispatch-xml/      # Redispatch 2.0 XML/XSD parsing — all 9 document types
│   ├── invoic-checker/      # INVOIC plausibility-check pipeline (LF side)
│   ├── mako-invoic/         # The INVOIC settle/dispute state machine, shared by all billing families
│   ├── mako-pruefung/       # Antwortnachricht decisions — the NB, LF, MSB, ESA, MaBiS and
│                            # NZR-EMob Entscheidungsbäume, each behind its own role feature
│   ├── mako-fristen/        # The German market calendar — Werktage, Fristen, and what "today" means
│   ├── energy-billing/      # LF consumption billing engine (§§40–41a EnWG)
│   ├── grid-billing/        # NB grid-fee billing — NNE/KA/MMM, §14a, Entgeltregime
│   ├── eeg-billing/         # EEG feed-in remuneration + Marktprämie
│   ├── mako-events/         # CloudEvents type catalog + matches()
│   ├── mako-markt/          # Market master-data domain (BO4E via rubo4e)
│   ├── mako-obs/            # Observability projections
│   └── mako-service/        # Service SDK — run::<D>() + Daemon/ServiceConfig · ServiceBuilder · load_config
│                            # DatabaseConfig · OidcConfig · CedarEnforcer · McpAuth · worker_lock · init_tracing_from_env
│
├── services/                # 17 daemons, one PostgreSQL schema each
│   ├── makod/               # :8080 · protocol daemon — AS4 ingest, workflow dispatch, EDIFACT render
│   ├── marktd/              # :8180 · master-data hub — BO4E store, MP-ID registry, event fan-out
│   ├── invoicd/             # :8280 · INVOIC plausibility check (LF)
│   ├── edmd/                # :8380 · energy data management — profiles, gap-fill, `?as_of` reads
│   ├── obsd/                # :8480 · observability — projections, KPIs, Fristen tracking
│   ├── processd/            # :8580 · process decision engine — STP checks, auto-responses
│   ├── netzbilanzd/         # :8680 · NB billing — NNE/KA/MMM/MSB INVOIC, REMADV, Redispatch Kostenblatt
│   ├── sperrd/              # :8780 · Sperrung execution tracking
│   ├── mabis-syncd/         # :8880 · MaBiS Summenzeitreihen submission (BIKO)
│   ├── productd/             # :9080 · tariffs & products — §41a dynamic pricing, Preisblätter
│   ├── einsd/               # :9180 · EEG remuneration — Marktprämie, Förderende alerts
│   ├── billingd/            # :9280 · LF customer billing — invoices, Abschläge, XRechnung/ZUGFeRD payloads
│   ├── accountingd/         # :9380 · sub-ledger (doubleentry) — Mahnwesen, §§41f/41g Sperr-Sequenz
│   ├── portald/             # :9480 · customer portal API
│   ├── agentd/              # :9580 · AI agent plane — 28 specialists over MCP, human oversight
│   ├── vertragd/            # :9780 · contract lifecycle — Lieferverträge, GGV, Aggregatoren
│   └── outputd/             # :9880 · document engine — Typst templates, ZUGFeRD carrier, publish gates, issued-document store + delivery
│
├── makotest/                # Python test toolkit (PyO3) — simulators, generators, pytest plugin
├── xtask/                   # Dev automation: import-profiles · validate · guards
└── fuzz/                    # 8 cargo-fuzz targets — EDIFACT parse/build/validate, AS4
                             # envelopes, OBIS codes, tariff inputs

Data flow

BDEW counterparty (AS4 push)
       │
       ▼
makod/as4_ingest  ──  asx-rs receive + WSS verify + dedup
       │
       ▼  raw EDIFACT bytes
Platform::parse_interchange  ──  edi-energy parse + validate
       │
       ▼  detected PID
PidRouter::route  ──  selects domain handler (GPKE / WiM / GeLi Gas / MABIS)
       │
       ▼  typed Command
Process::execute_and_enqueue  ──  replay state · Workflow::handle · AtomicAppend
       │
       ├─ EventStore (SlateDB)
       ├─ OutboxStore  ──►  OutboxErpWorker  ──►  makod ERP webhook (CloudEvents 1.0)
       ├─ OutboxStore  ──►  OutboxWorker     ──►  AS4 send → BDEW counterparty
       └─ DeadlineStore ──►  scheduler  ──►  TimeoutExpired → de.mako.aperak.timeout

                                          makod ERP webhook
                                                │ POST /api/v1/mako/events
                                                ▼
                                          marktd :8180 (Market Data Hub)
                                          MaLo / MeLo / contracts
                                          VersorgungsStatus · malo_grid
                                          PostgreSQL · OIDC/JWT
                                                │ fan-out (CloudEvents 1.0 + HMAC)
                               ┌────────────────┼──────────────┬──────────────┐
                               ▼                ▼              ▼              ▼
                         processd :8580   invoicd :8280   edmd :8380   obsd :8480
                         mako-pruefung    invoic-checker  meter reads  projections
                         NB STP · E_0624  §147 AO/GoBD    §60 MsbG     §7a parity
                               │                │
                               └────────────────┴──► makod :8080 (bestaetigen / ablehnen)
                               │
                               ▼
                         ERP system (SAP, Schleupen, Wilken, …)

⚙️ Feature Flags — edi-energy

By default UTILMD, MSCONS, APERAK, and CONTRL are compiled in:

cargo add edi-energy --features invoic,remadv,orders
Flag Default Enables
utilmd UTILMD Strom + Gas
mscons MSCONS metered consumption
aperak APERAK error acknowledgement
contrl CONTRL syntax acknowledgement
invoic INVOIC invoice
remadv REMADV remittance advice
orders ORDERS purchase order
iftsta IFTSTA multimodal status
insrpt INSRPT inspection report
reqote REQOTE request for quotation
partin PARTIN party information
ordchg ORDCHG order change
ordrsp ORDRSP order response
quotes QUOTES quotation
comdis COMDIS commercial dispute
pricat PRICAT price catalogue
utilts UTILTS technical master data
serde Serialize on EdiEnergyReport
diagnostics miette::Diagnostic on reports
tracing Structured tracing spans

⚙️ Feature Flags — dvgw-edi

cargo add dvgw-edi --features serde
Flag Default Enables
serde Serialize/Deserialize on all public types
tracing Structured tracing spans during parse dispatch

⚙️ Feature Flags — mako-markt

Flag Default Enables
(default) All domain types, all repository traits, CloudEvents, InboundMakoEvent
marktd-client HTTP client for marktd's REST surface
makod-client HTTP client for makod's command API
testing The two in-memory repository doubles (InMemoryVersorgungsStatusRepository, InMemoryNbEnergiemixRepository) — never enable in production

⚙️ Feature Flags — mako-engine

Flag Default Enables
slatedb Production SlateDbStore; makod turns it on through its own dependency (mako-engine = { features = ["slatedb"] }) — never enable it in a library's [features] defaults
testing InMemoryEventStore, NoopDeadLetterSink, InMemoryInboxStore — never in production
tracing Structured instrumentation spans

makod itself carries no storage flag. Its features are the ten role gates (role-{lf,nb,msb}-{strom,gas}, role-esa-strom and the three composites role-lf / role-nb / role-msb), which #[cfg]-gate production_modules so an NB-only binary contains no LF answer path. That is least-privilege deployment supporting §§ 6a/7a EnWG role separation, not itself the legal unbundling § 7 EnWG requires. See services/makod/ for the matrix.


🔧 Development

The justfile is the front door — every gate below has a recipe:

just            # list all recipes
just check      # cargo check, all targets & features
just test       # full test suite
just ci         # the complete CI gate (check + test + clippy incl. role-scoped builds + fmt + deny + profile/PID validation)
just test-db           # every real-PostgreSQL integration suite (testcontainers)
just test-accountingd-db  # …or one at a time: edmd, einsd, accountingd, billingd, outputd,
                          # vertragd, productd, marktd, processd, sperrd, invoicd,
                          # mabis-syncd, netzbilanzd, obsd, outbox

The test-*-db suites self-manage PostgreSQL via testcontainers — a throwaway postgres:17-alpine container is started in-process and reaped afterwards, so the only requirement is a running Docker daemon (no manual docker run, no DATABASE_URL). They are #[ignore]d by default and skip gracefully when Docker is absent.

Raw cargo equivalents:

# Check all targets — minimum gate before any commit
cargo check --all-targets --all-features

# Run all tests
cargo test --all-features

# Run tests for one crate
cargo test -p mako-engine --all-features

# Build the production daemon (slatedb is already enabled via mako-engine dep in Cargo.toml)
cargo build -p makod --release

# Lint (warnings are errors)
cargo clippy --all-targets --all-features -- -D warnings

# Format
cargo fmt --all

# Dependency audit (license + security)
cargo deny check

# The committed profiles are consistent: sources.json, dates, PIDs, AHB rows
cargo xtask validate-profiles

# Refuse banker's rounding: money and quantity figures round kaufmaennisch
# (DIN 1333, half away from zero). The modes differ only on exact midpoints,
# so the wrong one misstates a cent without failing an ordinary test.
cargo xtask check-rounding

# Refuse a Festlegung cited in a form it does not publish. BK6-22-024 numbers
# its operative part in Tenorziffern and carries its substance in Anlagen, so
# "BK6-22-024 para 4" names nothing a reader can look up - and an uncheckable
# citation is how a wrong Frist standing beside it survives review.
cargo xtask check-citations

# How much of the published Pruefidentifikator inventory the profiles carry, and
# whether the PID reference names all of it. validate-profiles compares
# consecutive releases, so it can prove nothing was lost and is blind to a PID
# that was never imported; this is the other direction. The inventory is
# extracted into crates/edi-energy/profiles/pid-overview.json, so this needs no
# source document.
cargo xtask check-pid-coverage

# Hold every Antwortcode against the published Entscheidungsbaum PDF —
# tree, code and Cluster (Zustimmung vs Ablehnung)
cargo xtask validate-ebd-codes

# Check that today's date is covered by a current profile
cargo xtask check-release-coverage

# Regenerate every profile from its BDEW MIG/AHB PDF (needs the mirror below)
cargo xtask import-profiles
cargo xtask import-profiles --check      # a committed profile drifted from its PDF

# Per Prüfidentifikator, what changed between two Formatversionen — the
# release PR's summary. Reads the committed profiles, no PDFs needed.
cargo xtask profile-diff utilmd fv20261001 fv20271001

# Mirror the BDEW document set every profile is read from
cargo xtask sync-regulatories            # report the diff against bdew-mako.de
cargo xtask sync-regulatories --download # fetch what is in force and missing
cargo xtask sync-regulatories --offline  # verify the mirror, no network

# Compile every fuzz target (the cheap gate, part of `just ci`)
just check-fuzz

# Run one target (requires nightly + cargo-fuzz)
cargo +nightly fuzz run fuzz_parse_validate

📊 Performance — edi-energy

Benchmarks on Apple M-series (single core, Criterion):

Operation Throughput
Parse minimal UTILMD ~2 µs / message
Validate UTILMD S2.1 (MIG + AHB) ~8 µs / message
Parse 100-message interchange ~180 µs total
Build UTILMD + serialize ~5 µs / message
cargo bench --bench benchmarks

🤝 Contributing

Contributions are welcome. Open an issue before large changes.

  • Run cargo check --all-targets --all-features and cargo test --all-features before submitting a PR.
  • The profiles under crates/edi-energy/profiles/ are generated from the BDEW PDFs — fix the reader in xtask/src/bdew/ and run cargo xtask import-profiles instead of editing JSON.
  • See the Release Lifecycle guide for the annual BDEW profile update procedure.
  • See the Process Engine guide for the engine architecture and conventions.

📜 License

Licensed under either of:

at your option.


🔗 Resources

  • edi-energy.de — Official BDEW specification portal
  • BDEW MaKo — Market communication framework
  • edifact-rs — Underlying EDIFACT parser
  • asx-rs — AS4/ebMS3 transport library used by makod
  • metering — German energy metering domain library (intervals, SLP/RLM classification, Gas m³→kWh_Hs); pure computation, no storage
  • meterstore — Metering time-series store (PostgreSQL hot window + Iceberg/S3 settled history) beneath edmd
  • doubleentry — General-purpose tamper-evident double-entry ledger used by accountingd
  • rubo4e — BO4E business-object types
  • billing — Generic EN 16931 tariff/invoicing engine under the settlement crates
  • SlateDB — Embedded LSM storage backing mako-engine

About

A Rust workspace for end-to-end German energy market communication (BDEW MaKo / EDI@Energy) — from raw EDIFACT bytes to production microservices.

Topics

Resources

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages