The legacy single-host monolith layout under src/ is gone. The canonical ASP.NET Core host is MimironsGoldOMatic.Backend.Api (Program.cs under src/MimironsGoldOMatic.Backend/MimironsGoldOMatic.Backend.Api/). Supporting code is in sibling Backend.* projects under src/MimironsGoldOMatic.Backend/ (.Application, .Infrastructure, .Infrastructure.Persistence, .Common, and .Configuration). Docker image: src/MimironsGoldOMatic.Backend/MimironsGoldOMatic.Backend.Api/Dockerfile. CI and local runbooks use dotnet … MimironsGoldOMatic.Backend.Api.csproj (see .github/workflows/e2e-test.yml, docs/setup/SETUP-for-developer.md).
MimironsGoldOMatic.Backend.Api, application/infrastructure projects) is the canonical integration service described in docs/overview/SPEC.md. It owns Twitch Extension JWT validation (HS256 via Twitch:ExtensionSecret, optional aud = ExtensionClientId), EventSub ingestion (channel.chat.message via POST /api/twitch/eventsub), Helix reward-sent messaging (§11, inline 3x retry in HelixChatService, no Outbox in MVP), and Desktop-facing routes protected by X-MGM-ApiKey.src/MimironsGoldOMatic.Backend/MimironsGoldOMatic.Backend.Api (plus Backend.* libraries) implements MVP-2 from docs/overview/ROADMAP.md: Marten on PostgreSQL, MediatR command/query handlers, Extension JWT + Desktop API key auth, EventSub chat enrollment, roulette/pool/payout APIs, Helix reward-sent announcement attempts, and hourly payout expiration. Configure ConnectionStrings:PostgreSQL, Mgm:ApiKey, and Twitch:* (appsettings.Development.json includes a local PostgreSQL example). EF Core is not currently used (read models are Marten documents).docs/reference/UI_SPEC.md (hub) and per-client docs/components/twitch-extension/UI_SPEC.md, docs/components/desktop/UI_SPEC.md, docs/components/wow-addon/UI_SPEC.md; API shapes remain canonical in docs/overview/SPEC.md.docs/overview/SPEC.md §6).Twitch:ExtensionSecret; Development fallback when secret empty. Issuer / JWKS-style rotation: roadmap.X-MGM-ApiKey matching Mgm:ApiKey (ApiKeyAuthenticationHandler).docs/overview/SPEC.md; this page explains implementation shape and operator run flow.channel.chat.message — ingest roulette enrollment !twgold <CharacterName> (enroll / replace name for same user per docs/overview/SPEC.md §5) and the separate gift queue command !twgift <CharacterName> (docs/overview/SPEC.md §12). Subscriber eligibility from the EventSub payload only (no Helix lookup on enroll); unique name among others; non-subscribers: log only (no chat reply). Dedupe by Twitch message_id. Acceptance is POST .../confirm-acceptance after [MGM_ACCEPT:UUID] in WoWChatLog.txt (see docs/overview/SPEC.md §9–10)./who: Desktop parses [MGM_WHO] from WoWChatLog.txt and forwards payload JSON to POST /api/roulette/verify-candidate. EBS is authoritative for deciding Pending vs no winner (no second candidate in the same 5-minute cycle; see docs/overview/SPEC.md §1, §5, §8).CharacterName in active pool; optional EnrollmentRequestId for Extension POST /api/payouts/claim.docs/overview/SPEC.md glossary, §5).Sent (may re-enroll via chat)./who <Winner_InGame_Nickname> before Pending payout; offline candidates invalid (no second pick same cycle — docs/overview/SPEC.md).docs/overview/SPEC.md §9 (Russian text + reply !twgold).confirm-acceptance after [MGM_ACCEPT:UUID] appears in WoWChatLog.txt (addon emits this after a matching Lua whisper !twgold; docs/overview/SPEC.md §9–10).Sent only when Desktop reports [MGM_CONFIRM:UUID] from the same log stream (mail actually sent), then remove the winner from the pool.Pending/InProgress older than 24 hours as Expired (no reactivation).Sent, the EBS must attempt Send Chat Message via Helix (inline 3× retry; no Sent rollback on failure; once per PayoutId — IsRewardSentAnnouncedToChat / transition guard, docs/overview/SPEC.md EBS + §6 + §11)./api/twitch/eventsub: EventSub webhook (AllowAnonymous + HMAC when secret set). channel.chat.message → ChatEnrollmentService (docs/overview/SPEC.md §5)./api/payouts/claim (optional): Extension/Dev Rig pool enrollment; Mgm:DevSkipSubscriberCheck gates Helix-less dev use (docs/overview/SPEC.md §5). Returns PoolEnrollmentResponse JSON on success./api/payouts/pending: Fetched by the Desktop App. Returns winner payouts available for sync/injection (primarily Pending)./api/payouts/{id}/status: Updates payout status where allowed (Desktop); response body PayoutDto./api/payouts/{id}/confirm-acceptance (recommended): Desktop reports !twgold matched the winner → record acceptance (not Sent)./api/payouts/my-last: Used by the Twitch Extension (pull model) to show the viewer their latest payout status.
404 Not Found when no payout exists for caller.GET /api/roulette/state, GET /api/pool/me (Extension JWT-only); POST /api/roulette/verify-candidate (Desktop ApiKey) — see docs/overview/SPEC.md §5–5.1.src/Tests/MimironsGoldOMatic.Backend.UnitTests (referenced from src/MimironsGoldOMatic.slnx). See src/Tests/MimironsGoldOMatic.Backend.UnitTests/README.md for coverage scope and coverlet.runsettings.dotnet test src/MimironsGoldOMatic.slnx --filter Category=Unit — time/spin phase, !twgold parser, controllers (Moq), ApiKey auth, Helix client, EventSub controller, FluentValidation.dotnet test src/MimironsGoldOMatic.slnx --filter Category=Integration — PostgreSQL via Testcontainers, Marten + MediatR (claims, chat enrollment, verify-candidate, expiration, payout status, roulette tick).dotnet test src/MimironsGoldOMatic.slnx — runs unit + integration; full suite needs Docker. Not a substitute for Twitch/WoW manual scenarios..github/workflows/e2e-test.yml runs Backend + Postgres + MockEventSubWebhook + MockExtensionJwt + MockHelixApi + SyntheticDesktop, synthetic EventSub enrollment, then .github/scripts/run_e2e_tier_b.py for Sent + Helix capture + pool removal. Formal success record: docs/e2e/E2E_AUTOMATION_PLAN.md — Tier B Final Validation & Success Report. Code layout: mocks in src/Mocks/ — PROJECT_STRUCTURE.md..github/scripts/tier_b_verification/ — health + MockHelixApi POST probe + optional SyntheticDesktop sequence (see Setting up Tier B Environment below).Tier B integration (summary): SyntheticDesktop (HTTP-only stand-in for WPF Desktop) calls the same REST routes as MimironsGoldOMatic.Desktop: confirm-acceptance, PATCH status InProgress → Sent. On Sent, EBS posts to Helix; in CI, Twitch:HelixApiBaseUrl points at MockHelixApi (9053), which records POST /helix/chat/messages for assertions. Development-only POST /api/e2e/prepare-pending-payout seeds a Pending payout when Mgm:EnableE2eHarness is true (see docs/e2e/E2E_AUTOMATION_PLAN.md).
Mirror the workflow on one machine (Linux/macOS/WSL or separate terminals on Windows):
mgm, user/password matching your connection string (same shape as CI: Host=localhost;Port=5432;Database=mgm;Username=postgres;Password=postgres).ASPNETCORE_ENVIRONMENT=Development
ASPNETCORE_URLS=http://127.0.0.1:8080
ConnectionStrings__PostgreSQL=Host=localhost;Port=5432;Database=mgm;Username=postgres;Password=postgres
Mgm__ApiKey=ci-desktop-api-key
Mgm__DevSkipSubscriberCheck=true
Twitch__EventSubSecret=<same secret as mocks>
then dotnet run --project src/MimironsGoldOMatic.Backend/MimironsGoldOMatic.Backend.Api/MimironsGoldOMatic.Backend.Api.csproj -c Release (or Debug).ASPNETCORE_URLS=http://127.0.0.1:9051
Backend__BaseUrl=http://127.0.0.1:8080
Twitch__EventSubSecret=<same as Backend>
then dotnet run --project src/Mocks/MockEventSubWebhook/MimironsGoldOMatic.Mocks.MockEventSubWebhook.csproj.ASPNETCORE_URLS=http://127.0.0.1:9052
Leave Twitch:ExtensionSecret empty only if Backend is in Development (shared dev key); otherwise set the same base64 Twitch:ExtensionSecret on both.
dotnet run --project src/Mocks/MockExtensionJwt/MimironsGoldOMatic.Mocks.MockExtensionJwt.csproj.python3 .github/scripts/send_e2e_eventsub.py --url http://127.0.0.1:9051 --secret "<secret>" --user-id e2e-viewer-1 --login e2eviewer1 --text "!twgold Etoehero"curl -s "http://127.0.0.1:9052/token?userId=e2e-viewer-1&displayName=E2EViewer" → use access_token as Authorization: Bearer … on GET http://127.0.0.1:8080/api/pool/me.Full checklist: docs/e2e/E2E_AUTOMATION_TASKS.md (Tier A Validation Checklist — all items verified; see Tier A Test Results).
Tier B adds MockHelixApi (loopback Helix stub on 9053), SyntheticDesktop (HTTP harness on 9054), and Twitch:HelixApiBaseUrl in HelixChatService / TwitchOptions. Use this sequence for local integration and for docs/e2e/E2E_AUTOMATION_PLAN.md Tier B First Run Guide.
pip install -r .github/scripts/tier_b_verification/requirements.txtASPNETCORE_URLS=http://127.0.0.1:9053
dotnet run --project src/Mocks/MockHelixApi/MimironsGoldOMatic.Mocks.MockHelixApi.csproj -c Release
Verify: python3 .github/scripts/tier_b_verification/check_mockhelixapi.pyASPNETCORE_URLS=http://127.0.0.1:9054
Mgm__ApiKey=ci-desktop-api-key (must match Backend)
SyntheticDesktop__BackendBaseUrl=http://127.0.0.1:8080
dotnet run --project src/Mocks/SyntheticDesktop/MimironsGoldOMatic.Mocks.SyntheticDesktop.csproj -c Release
Verify: python3 .github/scripts/tier_b_verification/check_syntheticdesktop.py
Full sequence (needs Pending payout): python3 .github/scripts/tier_b_verification/check_syntheticdesktop.py --payout-id <GUID>Twitch__HelixApiBaseUrl=http://127.0.0.1:9053, plus non-empty Twitch__BroadcasterAccessToken, Twitch__BroadcasterUserId, Twitch__HelixClientId so HelixChatService does not skip the outbound call. Restart Backend.python3 .github/scripts/tier_b_verification/check_workflow_integration.py (or --skip-tier-b if Tier B processes are stopped).References: docs/e2e/E2E_AUTOMATION_PLAN.md (Tier B Readiness Verification), Tier B Troubleshooting, docs/e2e/TIER_B_PRELAUNCH_CHECKLIST.md.
| Variable | Component | Purpose |
|---|---|---|
Twitch__HelixApiBaseUrl |
Backend | Points Helix HttpClient at MockHelixApi (e.g. http://127.0.0.1:9053). Requires A1–A2. |
Twitch__BroadcasterAccessToken |
Backend | Bearer for Helix; mock accepts any non-empty string unless strict auth is enabled. |
Twitch__BroadcasterUserId |
Backend | broadcaster_id / sender_id in JSON body. |
Twitch__HelixClientId |
Backend | Client-Id header; align with MockHelix__StrictAuth if used. |
MockHelix__StrictAuth |
MockHelixApi | If true, require Authorization: Bearer and Client-Id on POST /helix/chat/messages. |
SyntheticDesktop__BackendBaseUrl |
SyntheticDesktop | EBS root (default http://127.0.0.1:8080). |
Mgm__ApiKey |
Backend + SyntheticDesktop | Must match for X-MGM-ApiKey. |
Mgm__EnableE2eHarness |
Backend | true (Development only) enables POST /api/e2e/prepare-pending-payout for CI Tier B. |
ASPNETCORE_URLS |
All ASP.NET processes | 8080 Backend; 9051 / 9052 Tier A mocks; 9053 MockHelixApi; 9054 SyntheticDesktop. |
After Setting up Tier B Environment, either seed a Pending payout via POST /api/e2e/prepare-pending-payout (Development + Mgm:EnableE2eHarness) as in CI, or via enrollment → roulette tick → POST /api/roulette/verify-candidate with X-MGM-ApiKey.
POST http://127.0.0.1:9054/run-sequence with JSON {"payoutId":"<GUID>","characterName":"Etoehero"} (or use check_syntheticdesktop.py --payout-id / run_e2e_tier_b.py).Sent, GET http://127.0.0.1:9053/last-request should show the Russian §11 template message for the winner.GET http://127.0.0.1:9054/last-run if any step returns non-2xx.With Twitch__HelixApiBaseUrl unset, HelixChatService uses the production Twitch host — use MockHelixApi + base URL for local/CI validation.
MockEventSubWebhook returns 401: HMAC failed — align Twitch__EventSubSecret with --secret in send_e2e_eventsub.py; ensure the script’s JSON body is unchanged between signing and POST (compact JSON as in the script).Backend:BaseUrl or wrong path; check mock logs for EBS StatusCode.GET /api/pool/me 401: JWT signing key mismatch — in Development, both Backend and MockExtensionJwt must use empty ExtensionSecret (dev SHA256 fallback) or the same base64 secret; if Twitch:ExtensionClientId is set on Backend, set the same Twitch:ExtensionClientId on the mock so aud validates.GET /health on 9051 / 9052 → status ok and service field.GET /health on 9053 / 9054 → status healthy, component MockHelixApi / SyntheticDesktop.check_mockhelixapi fails: Ensure mock is on 9053; if using strict auth, send Bearer + Client-Id (script always sends them; enable strict only when Backend is wired).check_syntheticdesktop / run-sequence 502: Open GET /last-run on 9054 — steps lists HTTP status per call; compare with DesktopPayoutsController and domain transitions.check_workflow_integration timeout: Service not listening yet (slow dotnet run) or wrong port — see Tier B Troubleshooting — workflow integration.| Variable | Component | Purpose |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Backend | Development for dev Extension JWT key when secret empty. |
ASPNETCORE_URLS |
All ASP.NET processes | Bind addresses (8080, 9051, 9052). |
ConnectionStrings__PostgreSQL |
Backend | Marten / PostgreSQL. |
Mgm__ApiKey |
Backend | Desktop X-MGM-ApiKey routes (set for parity with Desktop tests). |
Mgm__DevSkipSubscriberCheck |
Backend | true so synthetic subscriber payload enrolls without Helix. |
Twitch__EventSubSecret |
Backend + MockEventSubWebhook | HMAC verification (empty = skip verification). |
Backend__BaseUrl |
MockEventSubWebhook | EBS root URL for forward. |
Twitch__ExtensionSecret |
Backend + MockExtensionJwt | Base64 symmetric key; empty + Development uses shared dev fallback string. |
Twitch__ExtensionClientId |
Backend + MockExtensionJwt | Optional JWT aud; must match if set. |
MartenMediatRFluentValidation.DependencyInjectionExtensionsMicrosoft.AspNetCore.Authentication.JwtBearerPolly.Extensions.Http (referenced; Helix retries are implemented as a simple 3-attempt loop in HelixChatService today)Idempotency Pattern:
Use EnrollmentRequestId as the idempotency key. If a network lag causes the extension to send the same request twice, the backend must return the existing record instead of creating a duplicate or consuming limits.
Outbox Pattern: Do not add an Outbox table in MVP. Helix §11 uses inline post-Sent calls (not Outbox). A future Outbox (e.g. Discord) is post-MVP — see docs/overview/SPEC.md §6.
Specification Pattern (Business Rules): Encapsulate business logic in Specification classes:
LifetimeLimitSpecification: Checks the 10k gold cap.ActiveRequestSpecification: Ensures only one active payout per Twitch user.
This makes business rules readable, testable, and reusable.PayoutCreated, PayoutStatusChanged, WinnerAcceptanceRecorded, HelixRewardSentAnnouncementSucceeded (Program.cs). This provides an append-only audit trail for payout lifecycle changes.