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 full NB STP demo stack locally and walks through the complete end-to-end flow: 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
Total time once the images are built: ~5 minutes. The first build takes longer — see Step 1, and rather longer again for the second demo, whose two services come out of the full builder stage.
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 .
Not
docker buildx bake—docker-bake.hclis the CI push path (push-by-digest), so it fails on the default docker driver and tagsghcr.io/hupe1980/mako-*rather than the:devimages the compose file starts.
The
processd-runtimestage builds with--features integrated(includes both the NBmako-pruefungand the LF answer modules).
These three come from a builder stage carrying only their dependencies — no Iceberg/DataFusion — so a cold build is a few minutes and a warm one under a minute.
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
What happens at startup:
processd self-registers its fan-out subscription with marktd on startup —
no manual subscription curl required. Both marktd and processd run SQLx
migrations automatically on first boot (databases are created by init-db.sh).
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 with its communicationchannels,rolesandcontacts. The bodies are not interchangeable, and both refuse a field they do not have — so a body sent to the wrong one is a422naming the field rather than a200with the values dropped.
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 Swagger UI | http://localhost:8080/api/v1/docs/ |
| makod MCP server | http://localhost:8080/mcp |
| marktd Swagger UI | http://localhost:8180/api/v1/docs/ |
| 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
The recipe rebuilds marktd too, which is a cache hit after the first demo.
edmd and einsd are the expensive half of this page: both copy from the full
builder stage, which compiles every service in the workspace 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: what the supplier sells, who bought it,
what they owe, the document that says so, and the receivable behind it. No
EDIFACT at all — demos/nb-stp is where a market message goes out; this is what
happens on the supplier's own books once supply is running.
| 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 deliberately absent — the smoke test names the
Netzbetreiber and supplies the meter reading, so no lookup happens. Their URLs
are configured to hostnames that do not resolve, so an override that is ever
dropped fails loudly instead of quietly 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. A wrong tariff version or
the other rounding direction fails the assertion.
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 one of the
seventeen services 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 observability, §20 parity | obsd guide |
| Full system architecture | Architecture |
| Process catalogue (GPKE, WiM, …) | Processes |
| mako-service SDK | mako-service README |