Getting Started
Run the mako NB straight-through-processing demo in about five minutes: submit a UTILMD 55001 Anmeldung and receive the 55002 confirmation back.
On this page 15 sections
- What you're running
- Prerequisites
- Step 1 — Clone and build
- Step 2 — Start the demo stack
- Step 3 — Verify health
- Step 4 — Seed master data
- Step 5 — Submit a UTILMD 55001
- Step 6 — Automatic NB decision
- Step 7 — Run the automated smoke test
- Step 8 — Explore the APIs
- Stop and clean up
- The second demo — EEG feed-in settlement
- The third demo — order-to-cash
- From source — one service, no images
- Next steps
This guide runs the NB STP demo stack locally: UTILMD 55001 → automatic NB decision → UTILMD 55002.
What you're running
| Service | Port | Role |
|---|---|---|
postgres | 5432 | PostgreSQL — one database per service |
webhook | 8000 | Demo ERP event receiver (Python, in-memory) |
marktd | 8180 | Market Data Hub — MaLo/MeLo/NeLo/TR, VersorgungsStatus, durable fan-out, event_log replay |
processd | 8580 | NB STP auto-responder — mako-pruefung (E_0622/G_0011), LF answers 55007/55010 inside their per-PID Frist |
makod | 8080 | EDIFACT process engine — GPKE/WiM/GeLi Gas, in-memory |
sequenceDiagram
participant LF as LF 4012345000023
participant makod as makod :8080
participant marktd as marktd :8180
participant processd as processd :8580
participant webhook as webhook :8000
LF->>makod: POST /edifact<br/>UTILMD 55001
makod-->>webhook: APERAK BGM+312<br/>(45-min deadline, auto)
makod->>marktd: de.mako.process.initiated<br/>HMAC POST /api/v1/events (demo override)
marktd->>processd: de.mako.process.initiated<br/>HMAC POST /webhook
marktd-->>webhook: de.mako.process.initiated<br/>(ERP subscription)
processd->>marktd: GET /api/v1/versorgung/{malo_id}
processd->>marktd: GET /api/v1/malos/{malo_id}/grid
processd->>marktd: GET /api/v1/partners/{lf_mp_id}
Note over processd: `mako-pruefung`: E_0622 → Accept (A51)
processd->>makod: gpke.lieferbeginn.bestaetigen
makod-->>webhook: UTILMD 55002 Bestätigung
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Docker | 24+ with Compose v2 | https://docs.docker.com/get-docker/ |
curl | any | OS package manager |
jq | any | OS package manager |
Step 1 — Clone and build
git clone https://github.com/hupe1980/mako.git
cd mako
just build-demo
just build-demo is the three builds the compose file needs, tagged the way it
expects them:
docker build --target runtime -t makod:dev .
docker build --target marktd-runtime -t marktd:dev .
docker build --target processd-runtime -t processd:dev .
docker buildx bake is the CI push path and does not produce the :dev
tags. The processd-runtime stage builds with --features integrated (the NB
and LF modules). The three come from a builder stage without Iceberg/DataFusion,
so a cold build takes a few minutes.
Step 2 — Start the demo stack
cd demos/nb-stp
docker compose up -d
docker compose ps # wait until all containers are running
Expected:
NAME IMAGE STATUS PORTS
nb-stp-postgres-1 postgres:17-alpine Up (healthy) 5432/tcp
nb-stp-webhook-1 python:3.12-alpine Up 0.0.0.0:8000->8000/tcp
nb-stp-marktd-1 marktd:dev Up 0.0.0.0:8180->8180/tcp
nb-stp-processd-1 processd:dev Up 0.0.0.0:8580->8580/tcp
nb-stp-makod-1 makod:dev Up 0.0.0.0:8080->8080/tcp
processd registers its fan-out subscription with marktd at startup, and
both run their migrations on first boot (init-db.sh creates the databases).
Step 3 — Verify health
curl -s http://localhost:8080/health | jq .
# → {"status":"ok","instance_id":"..."}
curl -s http://localhost:8180/health | jq .
# → {"status":"ok"}
curl -s http://localhost:8580/health/ready
# → 200 OK
Step 4 — Seed master data
processd's mako-pruefung needs three items in marktd to reach an Accept
decision. Every path below is relative to demos/nb-stp, the directory
Step 2 changed into.
4a — Price sheet
curl -s -X PUT http://localhost:8180/api/v1/preisblaetter/9900357000004 \
-H "Content-Type: application/json" \
--data-binary @fixtures/preisblatt-nb.json \
-w "\nHTTP %{http_code}\n"
# → HTTP 2044b — MaLo + MaLo grid record
MALO_ID=51238696012
# MaLo (NB=9900357000004, no active LF)
curl -s -X PUT "http://localhost:8180/api/v1/malos/$MALO_ID" \
-H "Content-Type: application/json" \
--data-binary "$(jq --arg m "$MALO_ID" '.data.marktlokationsId=$m' fixtures/malo-nb.json)" \
-w "\nHTTP %{http_code}\n"
# → HTTP 201
# MaLo grid record — without it the Anmeldung escalates instead of being answered
curl -s -X PUT "http://localhost:8180/api/v1/malos/$MALO_ID/grid" \
-H "Content-Type: application/json" \
-d '{"nb_mp_id":"9900357000004","bilanzierungsgebiet":"11YN0------0STXG","netzgebiet":"DEMO-NZ-001","sparte":"STROM","source":"manual"}' \
-w "\nHTTP %{http_code}\n"
# → HTTP 2044c — LF trading partner (E_0622 Prüfschritt 60)
# Register in marktd partner directory
curl -s -X PUT http://localhost:8180/api/v1/partners/4012345000023 \
-H "Content-Type: application/json" \
-d '{"display_name":"Demo LF","marktrolle":"LF","sparte":"STROM","makoadresse":["https://as4.example.com/as4/receive"]}' \
-w "\nHTTP %{http_code}\n"
# → HTTP 200
# Register in makod for EDIFACT routing
curl -s -X PUT http://localhost:8080/admin/partners/4012345000023 \
-H "Authorization: Bearer demo-secret-change-me" \
-H "Content-Type: application/json" \
--data-binary @fixtures/partner-lf.json | jq '.'
Two registries, two shapes.
marktdholds the partner's BO4EGeschaeftspartnerand its AS4 endpoints (makoadresse);makodholds the PARTIN record (channels,roles,contacts). Both refuse a field they do not have, so a body sent to the wrong one is a422naming the field.
Step 5 — Submit a UTILMD 55001
curl -s -X POST http://localhost:8080/edifact \
-H "Authorization: Bearer demo-secret-change-me" \
-H "Content-Type: text/plain; charset=utf-8" \
--data-binary @fixtures/utilmd-55001.edi | jq .
Expected response:
{
"accepted": 1,
"rejected": 0,
"messages": [{
"message_type": "UTILMD",
"pid": 55001,
"workflow": "gpke-supplier-change",
"status": "routed",
"process_id": "...",
"malo_id": "51238696012"
}]
}
Step 6 — Automatic NB decision
processd receives the de.mako.process.initiated event from marktd's
fan-out and walks the E_0622 Entscheidungsbaum (mako-pruefung) against the
master data seeded in Step 4.
# Check the decision log
curl -s http://localhost:8580/api/v1/decisions | jq '.[] | {
malo_id, decision, antwortcode, decided_at
}'
# → {"malo_id":"51238696012","decision":"Accept","antwortcode":"A51","decided_at":"..."}
With [nb] auto_accept = true (set in the demo's processd.toml), Accept automatically
dispatches gpke.lieferbeginn.bestaetigen to makod, which enqueues the outbound
UTILMD 55002 (Bestätigung Anmeldung verb. MaLo):
curl -s http://localhost:8000/events | jq '[.[] |
select(.body.makomessagetype=="UTILMD")
| {edifact: .body.data.edifact}
]'
Step 7 — Run the automated smoke test
smoke.sh runs all of the above and asserts every step:
bash smoke.sh
MARKTD_URL and WEBHOOK_URL default to what docker-compose.yml publishes.
Set either to the empty string to exercise makod alone.
Output ends with:
✓ processd NB auto-responder dispatched bestaetigen → UTILMD 55002 already arrived
✓ POST /api/v1/commands → HTTP 409 (duplicate bestaetigen correctly rejected — AntwortGesendet guard confirmed)
✓ UTILMD 55002 was already verified in step 6c (auto-responder path)
All smoke tests passed.
Wechselprozess auto-responder: ENABLED
Flow: UTILMD 55001 → makod → marktd ingest → validate → bestaetigen → UTILMD 55002
The HTTP 409 is the workflow state guard, not idempotency: processd
already moved the workflow to AntwortGesendet, so a second bestaetigen is an
out-of-order command and is rejected as invalid_state.
Step 8 — Explore the APIs
| Interface | URL |
|---|---|
| makod OpenAPI document | http://localhost:8080/api/v1/openapi.json |
| makod MCP server | http://localhost:8080/mcp |
| marktd OpenAPI document | http://localhost:8180/api/v1/openapi.json |
| marktd DLQ admin | http://localhost:8180/admin/fanout/dlq |
| marktd metrics | http://localhost:8180/metrics |
| processd decisions | http://localhost:8580/api/v1/decisions |
| processd approval queue | http://localhost:8580/api/v1/queue |
| ERP webhook event log | http://localhost:8000/events |
Stop and clean up
docker compose down # keep database volumes
docker compose down -v # destroy all volumes (full reset)
The second demo — EEG feed-in settlement
demos/eeg-billing runs the other half of the platform: no EDIFACT at all, but
the settlement path an NB owes an Anlagenbetreiber. einsd holds the plant
register and computes the § 21 EEG 2023 Einspeisevergütung; edmd holds the
quarter-hour Einspeisemenge it settles against.
| Service | Port | Role |
|---|---|---|
marktd | 8180 | Market Data Hub — the MaLo the plant feeds into |
edmd | 8380 | Energy Data Management — quarter-hour readings, billing periods |
einsd | 9180 | EEG/KWKG settlement — plant register, monthly Vergütung |
# from the repo root
just build-demo-eeg # marktd, edmd, einsd
cd demos/eeg-billing
docker compose up -d
bash smoke.sh
edmd and einsd copy from the full builder stage, which compiles every
service in one cargo build, Iceberg/DataFusion included: budget 20–45
minutes cold, a few minutes on a warm BuildKit cache.
It registers the Anlagenbetreiber and a 9.8 kWp rooftop plant behind it, pushes a
month of quarter-hour readings under OBIS 1-0:2.8.0, settles the month at EUR
233.568, and asserts the de.eeg.verguetung.berechnet CloudEvent the ERP
receives. The amount alone is not a legal document: under the Gutschriftverfahren
(§ 14 Abs. 2 Satz 2 UStG) the Netzbetreiber issues the Gutschrift, so einsd
renders it as a BO4E Rechnung whose VAT follows the operator's declared
ust_status — the fixture is a Kleinunternehmer (§ 19 UStG), so it carries 0 %
USt.
The third demo — order-to-cash
demos/o2c runs the retail money path on the supplier's own books — tariff,
contract, invoice, document, receivable — with no EDIFACT.
| Service | Port | Role |
|---|---|---|
productd | 9080 | Product and tariff catalogue — the only price source |
vertragd | 9780 | Kunden, Verträge, Kündigungsfristen |
billingd | 9280 | Multi-product billing engine (EN 16931) |
outputd | 9880 | Document store and delivery |
accountingd | 9380 | Massenkontokorrent — the double-entry ledger |
webhook | 8001 | ERP CloudEvent receiver, so this stack runs beside demos/nb-stp |
marktd, edmd and makod are absent: the smoke test names the
Netzbetreiber and supplies the meter reading, and their URLs point at hostnames
that do not resolve, so a dropped override fails instead of billing zero.
# from the repo root
just build-demo-o2c
cd demos/o2c
docker compose up -d
bash smoke.sh
It publishes a Tarifpreisblatt, creates a Kunde as a BO4E
Geschaeftspartner and a Versorgungsvertrag on a Marktlokation, bills one
month against that tariff, records the document in outputd for the § 147 AO
eight years, watches the invoice land in accountingd as an Offener Posten, and
closes it with a payment import.
§ 14 Abs. 4 Nr. 1 UStG makes the Leistungsempfänger's full name and address
part of the invoice, so step 3 reads the recipient back off both the BO4E
Rechnung and the EN 16931 model. A flat vorname/strasse/plz body is a
422 naming the field.
just test-demo-payloads deserialises every request body the demos post into
the real request type, and just test-demo-configs loads every *.toml the
stacks start with — no Docker, seconds.
The smoke test computes the expected amount itself: 20 ct/Tag × 31 Tage,
32 ct/kWh × 250 kWh, the § 3 StromStG Stromsteuer at 2.05 ct/kWh and an
8.00 ct/kWh Netznutzungsentgelt from the grid override, each line rounded to
the cent — 111.33 netto, 132.48 gross at 19 % USt.
From source — one service, no images
The demos run built images. To work on a service, run it from the checkout
instead; this needs Rust (installed from rust-toolchain.toml by rustup),
just and Docker for PostgreSQL:
just dev marktd
curl -s http://localhost:8180/health/ready
just dev starts PostgreSQL from dev/docker-compose.yml when it is not
running and starts the service with dev/config/marktd.toml; every service has
such a file, on its port from the services index. makod
needs no database — its dev event store is in memory — and agentd starts last,
because it connects to every other service's MCP endpoint at startup.
A config problem is reported in one go — the file read, the environment prefix
(MARKTD_, nested keys joined with __) and every missing, unknown or invalid
key. Two flags help before a first start:
cargo run -p marktd -- --help # usage and config conventions, reads no config
cargo run -p marktd -- --print-config # the merged file + environment, secrets redacted
just smoke-dev boots every service this way and waits for /health/ready.
CONTRIBUTING.md
covers building, testing one crate and running the gate.
Next steps
| Topic | Guide |
|---|---|
| EDIFACT parsing and validation | Parsing guide |
| ERP integration — CloudEvents, HMAC | ERP integration |
| makod operator reference | makod guide |
| marktd operator reference | marktd guide |
| processd — NB STP + LF answer automation | processd guide |
| INVOIC plausibility, § 147 AO / GoBD | invoicd guide |
| Energy data, imbalance, billing periods | edmd guide |
| Process read model, § 7a Abs. 5 EnWG parity | processd guide — process read model |
| Full system architecture | Architecture |
| Process catalogue (GPKE, WiM, …) | Processes |
| mako-service SDK | mako-service README |