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

This guide runs the NB STP demo stack locally: UTILMD 55001 → automatic NB decision → UTILMD 55002.

What you're running

ServicePortRole
postgres5432PostgreSQL — one database per service
webhook8000Demo ERP event receiver (Python, in-memory)
marktd8180Market Data Hub — MaLo/MeLo/NeLo/TR, VersorgungsStatus, durable fan-out, event_log replay
processd8580NB STP auto-responder — mako-pruefung (E_0622/G_0011), LF answers 55007/55010 inside their per-PID Frist
makod8080EDIFACT 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

ToolVersionInstall
Docker24+ with Compose v2https://docs.docker.com/get-docker/
curlanyOS package manager
jqanyOS 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 204

4b — 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 204

4c — 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. marktd holds the partner's BO4E Geschaeftspartner and its AS4 endpoints (makoadresse); makod holds the PARTIN record (channels, roles, contacts). Both refuse a field they do not have, so a body sent to the wrong one is a 422 naming 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

InterfaceURL
makod OpenAPI documenthttp://localhost:8080/api/v1/openapi.json
makod MCP serverhttp://localhost:8080/mcp
marktd OpenAPI documenthttp://localhost:8180/api/v1/openapi.json
marktd DLQ adminhttp://localhost:8180/admin/fanout/dlq
marktd metricshttp://localhost:8180/metrics
processd decisionshttp://localhost:8580/api/v1/decisions
processd approval queuehttp://localhost:8580/api/v1/queue
ERP webhook event loghttp://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.

ServicePortRole
marktd8180Market Data Hub — the MaLo the plant feeds into
edmd8380Energy Data Management — quarter-hour readings, billing periods
einsd9180EEG/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.

ServicePortRole
productd9080Product and tariff catalogue — the only price source
vertragd9780Kunden, Verträge, Kündigungsfristen
billingd9280Multi-product billing engine (EN 16931)
outputd9880Document store and delivery
accountingd9380Massenkontokorrent — the double-entry ledger
webhook8001ERP 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

TopicGuide
EDIFACT parsing and validationParsing guide
ERP integration — CloudEvents, HMACERP integration
makod operator referencemakod guide
marktd operator referencemarktd guide
processd — NB STP + LF answer automationprocessd guide
INVOIC plausibility, § 147 AO / GoBDinvoicd guide
Energy data, imbalance, billing periodsedmd guide
Process read model, § 7a Abs. 5 EnWG parityprocessd guide — process read model
Full system architectureArchitecture
Process catalogue (GPKE, WiM, …)Processes
mako-service SDKmako-service README

Edit this page ↗