Skip to content

Repository files navigation

Payment Switch — Master Repository

Demo

An eight-service, three-language simulation of a payment switch: participant banks and PSPs submit ISO 20022 payment messages, the switch authenticates, risk-scores, routes, ledgers, and settles them, and a response goes back to the caller.

This README is the single source of truth for the repo — architecture, message flow, topics, ports, sequence diagrams, how to run it, and an honest list of what's fixed vs. still open. (FINAL_ARCHITECTURE.md, referenced by an earlier version of this file, doesn't exist — consider this file its replacement.)

Companion documents:

  • INSTALL_AND_VERIFY.md — full step-by-step install, run, and feature-by-feature verification walkthrough.
  • GAPS_VS_REAL_WORLD.md — what it would actually take for this to operate as a licensed, global payment switch (regulatory, legal, and operational gaps — not code bugs).

Contents: 1. Service map · 2. Request flow · 3. Kafka topics · 4. Running it · 5. Known gaps · 6. Fixed this review · 7. Verified consistent · 8. Repo layout · 9. Sequence diagrams


1. Service map

Service Language Port Metrics Role
edge-gateway NestJS 8443 9091 mTLS/HTTPS participant interface, ISO 20022 ingestion
core-switch Go 8080 9097 Idempotency, transaction state, routing table, participant registry
elixir-orchestrator Elixir/OTP 4000 9093 Saga: Auth → Risk → Route → Ledger, one GenServer per transaction
auth-service Go 8081 9098 Participant limit checks (per-txn + daily caps)
risk-service Go 8082 9100 Velocity + amount risk scoring (APPROVE/SOFT_DECLINE/HARD_DECLINE)
go-ledger Go 8083 9094 Append-only double-entry ledger (Postgres)
go-settlement Go 8084 9095 Async multilateral netting + RTGS batch settlement
go-hsm Go 8085 9096 PKCS#11 sign / verify / MAC (SoftHSM2 in dev)

Each service also exposes /health, /ready, and /metrics (Prometheus). auth-service and risk-service are intentionally dependency-free (Go stdlib only) — see §5 for why, and §6 for what they actually do.


2. Request flow — the real saga order

The most important thing to know about this codebase: the saga is strictly sequential, not fan-out/fan-in. Auth, Risk, and Route run one after another, and Ledger only runs after the destination bank has confirmed the transaction. Settlement is asynchronous and happens after the customer already has a response.

Participant (bank app / PSP app / merchant POS / biller)
  |  HTTPS + mTLS, ISO 20022 payload
  v
edge-gateway :8443  --[Kafka: switch.inbound]-->  core-switch :8080
                                                     (dedupe / idempotency check)
                                                        |
                                          [Kafka: switch.orchestrate]
                                                        v
                                          elixir-orchestrator :4000
                                          one TxnProcess GenServer per transaction
                                                        |
        Step 1: AuthStep
        - HSM signature verify -> go-hsm :8085        POST /api/v1/verify
        - Participant limit check -> auth-service :8081  POST /v1/auth/verify
        Step 2: RiskStep
        - Velocity / amount score -> risk-service :8082  POST /v1/risk/score
        Step 3: RouteStep
        - Resolve destination -> core-switch :8080  POST /api/v1/routing/resolve
        - Dispatch -> Kafka partner.requests -> destination bank -> Kafka partner.responses
        Step 4: LedgerStep  (only runs after partner responds SUCCESS)
        - Double-entry write -> go-ledger :8083  POST /api/v1/entries
                                                        |
                              on saga success, two things fire in parallel:
                 -----------------------------------------------------------------
                 |                                                               |
                 v                                                               v
        Kafka switch.outbound                                       Kafka settlement.events
        -> edge-gateway -> HTTP response to caller                   -> go-settlement :8084
                                                                          (async netting -> RTGS)

Any saga step failure triggers compensation in reverse order (each step's compensate/1), not a raw error bubble-up.


3. Kafka topics & consumer groups

Topic Producer Consumer(s) Consumer group
switch.inbound edge-gateway core-switch core-switch
switch.inbound.dlq core-switch none wired up -
switch.orchestrate core-switch elixir-orchestrator orchestrator
partner.requests elixir-orchestrator destination bank / partner adapter -
partner.responses partner adapter elixir-orchestrator orchestrator-partner-resp
switch.outbound elixir-orchestrator edge-gateway edge-gateway-group
settlement.events elixir-orchestrator go-settlement settlement-engine
tx.ledger.append nothing publishes to this go-ledger ledger-service
tx.audit all services none (write-only audit trail) -

All 9 topics are created consistently by infrastructure/create-topics.sh and checked by verify-all.sh. Two of them are dead weight, not bugs:

  • tx.ledger.append: go-ledger has a full consumer for it, but nothing publishes to it — LedgerStep writes synchronously over HTTP instead. Harmless, just unused.
  • switch.inbound.dlq: created and written to on failure, but nothing currently consumes/alerts on it — worth wiring up a dead-letter monitor eventually.

4. Running it

# Start the full stack
docker-compose -f infrastructure/docker-compose.yml up -d

# Wait for all services to report healthy (~60s)
sleep 60

# Sanity-check every service and topic
bash verify-all.sh

# Send a test payment (no X-Signature — a non-empty one triggers real HSM
# verification against a per-participant key that won't exist yet; see the
# full install/verify guide for how to generate one and sign for real)
curl -X POST http://localhost:8443/v1/gateway/message \
  -H "Content-Type: application/json" \
  -H "X-Participant-ID: HDFC" \
  -H "X-Nonce: $(uuidgen)" \
  -H "X-Timestamp: $(date +%s%3N)" \
  -d '{
    "messageType": "payment.request",
    "payload": "<pain.001/>",
    "data": {
      "idempotencyKey": "e2e-001",
      "amount": 150000,
      "currency": "INR",
      "debtorAccount": "1234567890",
      "creditorAccount": "0987654321",
      "debtorBank": "HDFC",
      "creditorBank": "SBI"
    }
  }'

Before any of this will work, see the "required setup" and "critical gap" items below — as committed, the repo won't build (missing lockfiles) and a submitted transaction won't complete the saga (missing auth/risk handlers).

Observability

Tool URL
Jaeger (traces) http://localhost:16686
Prometheus http://localhost:9099
Grafana http://localhost:3000

5. Known gaps — read before assuming this "just works"

Critical — no transaction could complete the saga end-to-end — fixed

Previously, AuthStep's participant-limit check and all of RiskStep called AUTH_SERVICE_URL/RISK_SERVICE_URL, both pointed at http://core-switch:8080 — a service with no handler for either route. Every call 404'd and the saga halted on every transaction.

This is now fixed the way elixir-orchestrator/ARCHITECTURE.md always intended: two real, standalone services — auth-service and risk-service — added to the compose stack, with AUTH_SERVICE_URL/RISK_SERVICE_URL pointed at them instead of core-switch. No changes were needed on the orchestrator side — its calls to /v1/auth/verify, /v1/risk/score, and /v1/risk/decrement already matched exactly once something was actually listening. See §6 for what each one does.

Required setup before the repo builds

None of the five backend services ship a lockfile, but every Dockerfile expects one:

  • core-switch, go-ledger, go-settlement, go-hsm — no go.sum, but each Dockerfile does COPY go.mod go.sum ./.
  • elixir-orchestrator — no mix.lock, but its Dockerfile does COPY mix.exs mix.lock ./.

docker build will fail immediately on all five with "file not found." Run go mod tidy in each Go service and mix deps.get in the orchestrator (needs normal internet access) once, commit the resulting lockfiles, and this goes away permanently.

Minor / cosmetic

  • ps-e2e (the Go integration test module) also has no go.sum — same fix.
  • switch.inbound.dlq has no consumer/alerting wired up yet.
  • edge-gateway has 65 remaining npm audit findings (28 moderate, 34 high, 3 low) in @nestjs/*, @opentelemetry/*, and multer — all in packages actually used in the code (unlike the two criticals already removed, see §6 item 10). Fixing these means a major-version bump (npm audit fix --force), which is a real upgrade to plan/test rather than a safe drop-in — not done here.

6. Fixed in the most recent review pass

These were found and corrected in this repo (not just documented):

  1. The saga couldn't complete a single transaction. AuthStep and RiskStep called an auth-service/risk-service that didn't exist anywhere in the repo — every call 404'd against core-switch, which was standing in for both. Built two real services instead of patching around it:
    • auth-service (POST /v1/auth/verify) — checks a submitted amount against a per-participant table of per-transaction and rolling-daily limits (seeded with HDFC/SBI/ICICI/AXIS plus a conservative default for unknown participants), returns 403 with a reason on suspension/limit breach, 200 with the remaining daily headroom otherwise.
    • risk-service (POST /v1/risk/score, POST /v1/risk/decrement) — scores each transaction from two signals: a 60-second sliding-window velocity count per source_account, and an amount tier. Combined score maps to APPROVE (score < 0.7), SOFT_DECLINE (0.7–0.9), or HARD_DECLINE (≥ 0.9) — matching RiskStep's expected decision strings exactly. /v1/risk/decrement undoes the last velocity hit for compensation.
    • Both are dependency-free (Go stdlib only, net/http/encoding/json — no external module needed at all), specifically so they don't inherit the missing-go.sum problem described in §5 below, and so I could actually compile and functionally test them in this environment rather than just writing code that looked plausible. Both were run and hit with real requests before being wired in.
    • Wired into docker-compose.yml on ports 8081/8082, with health checks, and added to the orchestrator's depends_on. Zero changes needed to the Elixir saga code — its calls already matched these exactly once something real was listening.
  2. edge-gateway wouldn't npm install@opentelemetry/api@^1.8.0 conflicted with @opentelemetry/sdk-node@0.48.0's peer requirement (<1.8.0). Pinned to ^1.7.0.
  3. edge-gateway was missing dependenciesredis was imported in code but never declared (only ioredis was); @types/express was missing too. Both added.
  4. edge-gateway/Dockerfile never copied package-lock.json into the build context before running npm ci. Fixed.
  5. docker-compose port collisioncore-switch's metrics port was mapped to the same host port as Kafka's broker port (9092:9090 vs. Kafka's 9092:9092), which would prevent both containers from starting. Remapped core-switch to 9097:9090.
  6. Every healthcheck in the stack was broken. core-switch, go-ledger, and go-settlement run on distroless/static images (no shell, no wget — the CMD-SHELL wget ... healthchecks couldn't even execute). hsm-connector and orchestrator run on debian-slim but never had wget installed. Since edge-gateway, orchestrator, and settlement-engine all depend on these via condition: service_healthy, docker-compose up would have hung indefinitely waiting for containers that could never report healthy. Fixed by adding a tiny stdlib-only Go healthcheck binary to each distroless image (exec-form, no shell needed) and adding wget to the two debian-slim images.
  7. edge-gateway's own health endpoint was unreachableapp.setGlobalPrefix('v1') had no exclusions, so /health and /ready only existed at /v1/health / /v1/ready, while every healthcheck and verify-all.sh hit the unprefixed path (404, always). Fixed with exclude: ['health', 'ready'].
  8. Go source formatting — all four original Go services were unformatted; ran gofmt -w across all of them (confirmed via gofmt -d these were purely cosmetic, not syntax errors).
  9. Repo cleanup — removed a large number of stray empty directories left over from a brace-expansion mistake (e.g. go-ledger/internal/{config,domain,...} as a literal directory name) scattered across four services, plus a stale FINCH_POOL_PATCH.ex leftover note in elixir-orchestrator (the fix it described was already correctly applied in application.ex).
  10. Two critical CVEs, both dead code. npm audit on edge-gateway flagged 67 vulnerabilities; the two critical ones were xml-crypto (signature verification bypass / spoofing — GHSA-...) and xmldom (multiple XML injection advisories, no fix available). Checked whether either is actually reachable: neither is imported anywhere in src/ — both were declared in package.json but never used (signature verification actually goes through go-hsm, not a local XML library). Removed both entirely rather than chasing a fix for code that doesn't run — regenerated the lockfile and confirmed the build and CVE count both come out clean (67 → 65, both criticals gone). The remaining 65 (28 moderate, 34 high, 3 low) are in @nestjs/*, @opentelemetry/*, and multer — all actually used, all require a major-version bump to fix (npm audit fix --force, breaking changes), and weren't force-upgraded here since that's a real upgrade to plan and test, not a drop-in patch. Worth doing as a follow-up, particularly the @nestjs/core injection advisory and the multer DoS advisories.
  11. ioredis was dead weight — declared as a dependency but never imported anywhere in src/ (the code that actually talks to Redis, replay.prevention.ts, uses the redis package). Removed it; one less thing to patch, one less thing to be confused about next time someone reads this codebase.
  12. ledger-redis (port 6380) had zero monitoring. There are two Redis instances in this stack — redis:6379 (core-switch idempotency, edge-gateway replay prevention) and ledger-redis:6380 (go-ledger's balance cache) — but only one redis-exporter existed, pointed at the first. The second was completely dark to Prometheus. Added a second exporter (ledger-redis-exporter) and a matching scrape job.
  13. Two of three Prometheus alert-rule files were silently being dropped. docker-compose.yml mounted go-ledger/monitoring/alerts, go-settlement/monitoring/alerts, and go-hsm/monitoring/alerts all as directory volumes to the same container path (/etc/prometheus/alerts). Docker doesn't merge directory mounts to an identical target — only the last one in the list (go-hsm's) actually appeared in the container, so ledger_alerts.yml and settlement_alerts.yml were never loaded despite looking correctly configured. Switched all three to file-level mounts (each .yml to its own path within the same target directory), which Docker does support side by side.
  14. Grafana had no datasource at all. It ran with anonymous access enabled but no provisioning and no volume — opening it showed an empty instance, and any datasource added by hand through the UI wouldn't have survived a container restart anyway (nothing was persisting it). Added infrastructure/grafana/provisioning/datasources/datasources.yml (Prometheus + Jaeger, both auto-configured on boot) and mounted it in.
  15. verify-all.sh's Kafka checks would fail for most people, for two compounding reasons. It called kafka-topics.sh/kafka-consumer-groups.sh as if they were installed on the host — most machines don't have the Confluent CLI tools lying around, and this was never listed as a prerequisite anywhere. And even with them installed, Kafka only advertised itself as kafka:9092 (KAFKA_ADVERTISED_LISTENERS) — a host-side client would get past the initial bootstrap to localhost:9092 but fail on every real request once it followed up against an address that only resolves inside the docker network. Fixed both: verify-all.sh now runs the CLI tools inside the kafka container itself (docker compose exec kafka ..., no host install needed), and Kafka now has a genuine second listener (localhost:29092) for anyone who does want to point their own local tools directly at the broker.
  16. otel-collector exposed a port nothing was listening on. docker-compose.yml published 13133 (the conventional OTel Collector health-check port), but collector.yml never actually configured the health_check extension — same "port exposed, nothing behind it" pattern as the earlier auth-service/risk-service metrics-port bug. Added the extension so the port is real. Deliberately did not add a Docker HEALTHCHECK for this one, though — the otel/opentelemetry-collector-contrib image is minimal (likely no shell, same as the distroless Go images), and adding a CMD-SHELL probe without being able to verify that first would risk reintroducing exactly the broken-healthcheck bug from item 6. curl http://localhost:13133 from the host now genuinely works if you want to check it by hand.

7. Verified consistent (checked, not just assumed)

  • settlement.events message contract: the Elixir producer's fields match Go's SettlementEvent struct tag-for-tag.
  • All 9 Kafka topic names and all 6 consumer group names match exactly across create-topics.sh, verify-all.sh, and every service's actual code — no drift.
  • HSM caller identities (edge-gateway, core-switch, elixir-orchestrator, settlement-engine) match ALLOWED_CALLERS in docker-compose exactly.
  • RouteStep -> core-switch (/api/v1/routing/resolve) and LedgerStep -> go-ledger (/api/v1/entries, /api/v1/entries/reverse) and the HSM signature-verify call (/api/v1/verify) all match their target service's actually-registered routes.
  • AuthStep -> auth-service (/v1/auth/verify) and RiskStep -> risk-service (/v1/risk/score, /v1/risk/decrement) — actually built, compiled, and hit with real requests (approve / per-txn-limit reject / unknown-participant / velocity escalation to SOFT_DECLINE and HARD_DECLINE) before being wired into compose.
  • No other duplicate host ports or Dockerfile path/COPY mismatches found.
  • Every DB connection env var (POSTGRES_HOST/PORT/USER/PASSWORD/DB) that core-switch, go-ledger, and go-settlement read matches what's actually set in docker-compose.yml, and matches the right Postgres instance (postgres for core-switch; ledger-postgres, shared, for go-ledger + go-settlement).
  • Every REDIS_ADDRS/REDIS_HOST env var read by core-switch, go-ledger, and edge-gateway matches what's set, and each points at the right one of the two Redis instances (redis:6379 vs. ledger-redis:6380).
  • The OTel collector's own config (infrastructure/otel/collector.yml) exports to jaeger:14250, matching Jaeger's actual exposed port; all 6 services that reference the otel-env anchor (correctly excluding the 2 dependency-free new services) actually initialize an OTel exporter in their own code (internal/observability/tracer.go in each Go service).
  • kafka-setup's broker address, the migration files each Postgres container mounts, and every alert-rule file Prometheus expects to find, all actually exist on disk at the paths referenced.

8. Repo layout

edge-gateway/          NestJS   - mTLS ingress, ISO 20022 -> internal format
core-switch/           Go       - idempotency, routing table, participant registry
elixir-orchestrator/   Elixir   - saga (Auth -> Risk -> Route -> Ledger)
auth-service/          Go       - participant limit checks (stdlib only, no deps)
risk-service/          Go       - velocity + amount risk scoring (stdlib only, no deps)
go-ledger/             Go       - double-entry ledger (Postgres)
go-settlement/         Go       - async netting + RTGS batch settlement
go-hsm/                Go       - PKCS#11 signing/verification (SoftHSM2 in dev)
ps-e2e/              Go       - end-to-end integration tests
infrastructure/        docker-compose.yml, Kafka topic setup, Prometheus config,
                       Grafana datasource provisioning, OTel collector config
docs/sequence-diagrams/ Mermaid source for every flow diagrammed in §9
verify-all.sh          Post-startup smoke test across every service and topic

Each service also has its own ARCHITECTURE.md with service-specific design notes (circuit breakers, connection pooling, retry policy, etc.) — this file is the cross-service picture.


9. Sequence diagrams

Mermaid source for each — renders automatically on GitHub, or paste into https://mermaid.live to view/edit. The same source files live standalone at docs/sequence-diagrams/*.mermaid.

9.1 Happy-path payment — full saga to settlement

sequenceDiagram
    autonumber
    participant P as Participant Bank/PSP
    participant EG as edge-gateway
    participant K as Kafka
    participant CS as core-switch
    participant OR as orchestrator
    participant HSM as go-hsm
    participant AU as auth-service
    participant RI as risk-service
    participant DB as Destination Bank
    participant LE as go-ledger
    participant SE as go-settlement

    P->>EG: POST /v1/gateway/message (ISO 20022)
    EG->>K: publish switch.inbound
    K->>CS: consume switch.inbound
    CS->>CS: idempotency check (dedupe on key)
    CS->>K: publish switch.orchestrate
    K->>OR: consume switch.orchestrate
    Note over OR: one TxnProcess GenServer per transaction

    rect rgb(240, 248, 255)
    Note over OR,HSM: Step 1 — AuthStep
    OR->>HSM: POST /api/v1/verify (signature)
    HSM-->>OR: 200 valid
    OR->>AU: POST /v1/auth/verify (participant limit check)
    AU-->>OR: 200 approved, remaining_daily_limit
    end

    rect rgb(255, 250, 240)
    Note over OR,RI: Step 2 — RiskStep
    OR->>RI: POST /v1/risk/score
    RI-->>OR: 200 {score, decision: APPROVE}
    end

    rect rgb(240, 255, 240)
    Note over OR,DB: Step 3 — RouteStep
    OR->>CS: POST /api/v1/routing/resolve
    CS-->>OR: destination route
    OR->>K: publish partner.requests
    K->>DB: deliver payment request
    DB->>K: publish partner.responses (SUCCESS)
    K->>OR: consume partner.responses
    end

    rect rgb(255, 240, 245)
    Note over OR,LE: Step 4 — LedgerStep (only after partner SUCCESS)
    OR->>LE: POST /api/v1/entries (double-entry write)
    LE-->>OR: 200 posted
    end

    par response to customer
        OR->>K: publish switch.outbound
        K->>EG: consume switch.outbound
        EG-->>P: HTTP 200 {correlation_id, status: ACCEPTED}
    and async settlement (does not block the customer response)
        OR->>K: publish settlement.events
        K->>SE: consume settlement.events
        SE->>SE: update net position (per participant pair, currency)
    end
Loading

9.2 Decline paths and compensation

sequenceDiagram
    autonumber
    participant P as Participant Bank/PSP
    participant EG as edge-gateway
    participant K as Kafka
    participant OR as orchestrator
    participant AU as auth-service
    participant RI as risk-service

    P->>EG: POST /v1/gateway/message
    EG->>K: publish switch.inbound
    K->>OR: (via core-switch → switch.orchestrate)

    alt Auth step rejects (limit exceeded / suspended)
        OR->>AU: POST /v1/auth/verify
        AU-->>OR: 403 {approved: false, reason}
        Note over OR: no prior steps to compensate — saga halts immediately
        OR->>K: publish switch.outbound (DECLINED, reason)
        K->>EG: consume switch.outbound
        EG-->>P: HTTP 200 {status: DECLINED, reason}
    else Auth approves, Risk hard-declines
        OR->>AU: POST /v1/auth/verify
        AU-->>OR: 200 approved
        OR->>RI: POST /v1/risk/score
        RI-->>OR: 200 {score >= 0.9, decision: HARD_DECLINE}
        OR->>RI: POST /v1/risk/decrement (compensate: undo velocity hit)
        RI-->>OR: 200
        OR->>K: publish switch.outbound (DECLINED, reason)
        K->>EG: consume switch.outbound
        EG-->>P: HTTP 200 {status: DECLINED, reason}
    else Destination bank rejects at Route step
        Note over OR: Auth + Risk already passed
        OR->>OR: RouteStep dispatches, partner.responses = FAILURE
        OR->>RI: POST /v1/risk/decrement (compensate)
        Note over OR: LedgerStep never runs — no entry to reverse
        OR->>K: publish switch.outbound (DECLINED, reason: partner rejected)
        K->>EG: consume switch.outbound
        EG-->>P: HTTP 200 {status: DECLINED, reason}
    end
Loading

Note: AuthStep.compensate/1 is a documented no-op ("Auth is read-only — no side-effects to undo") — but auth-service does increment a rolling daily usage counter on approval, and there's no corresponding decrement endpoint. A transaction that passes Auth but fails later (Risk/Route/Ledger) currently consumes that participant's daily limit headroom permanently. Small, real gap — flagged here rather than fixed, since fixing it means adding a genuine /v1/auth/decrement endpoint and deciding the right semantics for it.

9.3 Idempotent duplicate submission

sequenceDiagram
    autonumber
    participant P as Participant Bank/PSP
    participant EG as edge-gateway
    participant CS as core-switch

    Note over P,CS: First submission
    P->>EG: POST /v1/gateway/message (idempotencyKey = X)
    EG->>CS: publish switch.inbound
    CS->>CS: lookup idempotencyKey X → not found
    CS->>CS: store X → PROCESSING, forward to orchestrator
    CS-->>EG: 202 accepted
    EG-->>P: HTTP 200 {correlation_id, status: ACCEPTED}

    Note over P,CS: Retry with the same idempotency key (network timeout, client retry, etc.)
    P->>EG: POST /v1/gateway/message (idempotencyKey = X, identical payload)
    EG->>CS: publish switch.inbound
    CS->>CS: lookup idempotencyKey X → found (already processed/processing)
    CS-->>EG: original result — NOT reprocessed, no second orchestrator run
    EG-->>P: HTTP 200 — same correlation_id and status as the first call
Loading

9.4 HSM key generation, signing, and verification

sequenceDiagram
    autonumber
    participant Op as Operator (one-time setup)
    participant HSM as go-hsm
    participant EG as edge-gateway
    participant P as Participant Bank/PSP
    participant OR as orchestrator (AuthStep)

    Note over Op,HSM: One-time: generate a signing/verify key for a participant
    Op->>HSM: POST /api/v1/keys/generate<br/>{id: "HDFC:verify-key", algorithm, purpose}
    HSM-->>Op: 200 key created

    Note over P,HSM: Participant signs their payload before sending
    P->>HSM: POST /api/v1/sign<br/>{key_id, payload, algorithm: ECDSA-SHA256}
    HSM-->>P: 200 {signature}

    P->>EG: POST /v1/gateway/message<br/>headers: X-Signature = signature
    EG->>OR: (forwarded through switch.inbound → switch.orchestrate)

    Note over OR,HSM: AuthStep — Step A, signature verification
    OR->>HSM: POST /api/v1/verify<br/>{key_id, payload, signature}
    alt signature valid
        HSM-->>OR: 200 {valid: true}
        Note over OR: proceeds to Step B (participant limit check)
    else signature invalid / wrong key
        HSM-->>OR: 200 {valid: false} or 401
        OR->>OR: saga halts — request rejected
    end
Loading

9.5 Settlement netting cycle (async, decoupled from the saga)

sequenceDiagram
    autonumber
    participant OR as orchestrator
    participant K as Kafka
    participant SE as go-settlement
    participant PG as ledger-postgres (shared with go-ledger)
    participant Op as Operator / scheduler

    Note over OR,PG: Continuous — one event per completed transaction
    loop for every transaction that reaches LedgerStep successfully
        OR->>K: publish settlement.events<br/>{source_bank, destination_bank, amount, currency, ...}
        K->>SE: consume settlement.events (group: settlement-engine)
        SE->>PG: update running net position<br/>(per participant pair, per currency)
    end

    Note over Op,SE: Separately — a settlement cycle is triggered on a schedule or manually
    Op->>SE: POST /api/v1/settlement/trigger {cycle_type: T0, currency}
    SE->>PG: read current net positions for currency
    SE->>SE: compute multilateral net obligations per participant
    Note over SE: In this repo, this step is simulated —<br/>see GAPS_VS_REAL_WORLD.md §3 for what real RTGS<br/>connectivity and settlement-finality guarantees require
    SE->>PG: mark included positions as settled for this cycle
    SE-->>Op: 200 {cycle_id, positions_settled, net_amounts}
Loading

About

An eight-service, three-language simulation of a national payment switch : participant banks and PSPs submit ISO 20022 payment messages, the switch authenticates, risk-scores, routes, ledgers, and settles them, and a response goes back to the caller.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages