MimironsGoldOMatic

12) Gift Queue (!twgift) — streamer-scoped serialized flow

The !twgift <CharacterName> flow is a separate queue from payout roulette and is persisted in Marten/PostgreSQL.

Gift request states

Pending -> SelectingItem -> WaitingConfirmation -> Completed

Timeouts

API endpoints

Addon/Desktop contract additions

Mimiron’s Gold-o-Matic — Technical Specification (MVP)

This document is the canonical implementation contract for the MVP. docs/overview/ROADMAP.md contains step-by-step prompts and links into this spec. User-facing UI: hub docs/reference/UI_SPEC.md (tokens, navigation); per-surface screens in docs/components/twitch-extension/UI_SPEC.md, docs/components/desktop/UI_SPEC.md, docs/components/wow-addon/UI_SPEC.md.

Code alignment: MVP slices MVP-1 … MVP-5 are implemented under src/ (Shared, src/MimironsGoldOMatic.Backend/MimironsGoldOMatic.Backend.Api plus sibling Backend.* projects, Desktop, Twitch Extension, WoW addon). Remaining gaps (automated tests, packaging, production hardening) are summarized in docs/reference/IMPLEMENTATION_READINESS.md.

Non-normative digests (do not override this file): docs/overview/MVP_PRODUCT_SUMMARY.md, docs/reference/GLOSSARY.md, docs/reference/WORKFLOWS.md, docs/overview/ARCHITECTURE.md.

EBS — Extension Backend Service (normative, MVP)

Helix §11 reward-sent delivery (MVP, locked)

MVP deployment scope (normative)

1) Glossary

2) MVP economics & anti-abuse rules

3) Statuses & lifecycle transitions

Status enum (MVP)

Allowed transitions (normative)

From To Who/when
Pending InProgress Desktop on Sync/Inject, only after WoW target is detected (§8 Desktop → WoW injection)
Pending Cancelled Desktop (streamer)
Pending Failed Desktop (streamer)
InProgress Sent Desktop observes [MGM_CONFIRM:UUID] in Logs\WoWChatLog.txt and calls Backend (PATCH status or dedicated endpoint); or manual Mark as Sent if policy allows
InProgress Cancelled Desktop (streamer)
InProgress Failed Desktop (streamer)
InProgress Pending Desktop (escape hatch — e.g. unlock queue after failed inject; streamer policy)
Pending/InProgress Expired Backend hourly expiration job

4) Identity, idempotency, and DTOs

Identity

CharacterName validation (MVP, normative)

Shared rules for chat enrollment, POST /api/payouts/claim, and server-side checks:

Implement with FluentValidation in MimironsGoldOMatic.Shared (same rules Backend + Desktop).

Idempotency

MVP behavior on duplicate EnrollmentRequestId (Extension only):

5) API Contract (MVP)

This section defines the MVP endpoints and semantics. GET /api/roulette/state and GET /api/pool/me field lists in §5.1 are normative for MVP. Other illustrative JSON bodies remain guidance until OpenAPI/schemas are locked; behavior is normative.

Common headers

Twitch EventSub webhook — chat enrollment (MVP, implemented)

Development configuration — Extension POST /api/payouts/claim vs chat enrollment

Participant pool & roulette (normative behavior)

Implementation note: Chat ingestion for enrollment uses EventSub channel.chat.message (see glossary); document reconnect/backfill policy. Acceptance is WoW whisper !twgold (§9). Pool + spin schedule for the Extension are normative in §5.1. /who run/parse is addon; report transport is chat log line DEFAULT_CHAT_FRAME:AddMessageWoWChatLog.txt → Desktop (§8, §10).

Error model (MVP)

Recommended JSON error shape (server should be consistent):

{
  "code": "active_payout_exists",
  "message": "User already has an active payout.",
  "details": {}
}

HTTP overload: Under load or upstream saturation, the API may return 503 Service Unavailable (hosting-dependent). Twitch Extension clients should treat 503 like 429 and apply §5.1 (backoff + Retry). Global queue depth is not normative beyond per-identity rate limits.

Recommended code values (MVP):

Twitch chat commands (normative product contract)

Chat message (broadcast) When Effect
!twgold <CharacterName> (prefix case-insensitive) Subscriber; name available Add/update pool entry for this viewer with CharacterName
!twgold with no <CharacterName> (wrong arity) Ignore (no pool change; no error required)
Other !twgold ... variants Ignore unless explicitly supported later

Acceptance is not via Twitch chat in MVP; see WoW table in §9.

Parsing: treat the !twgold enrollment prefix as case-insensitive; compare normalized message start after trim.

POST /api/payouts/claim (optional; Extension / Dev Rig)

Purpose: optional path to add a subscriber to the participant pool when not using chat-only enrollment (same rules as !twgold <CharacterName>).

Auth: Twitch Extension JWT (Bearer), same scheme as GET /api/roulette/state.

Request (normative shape; camelCase JSON):

{
  "characterName": "Somecharacter",
  "enrollmentRequestId": "550e8400-e29b-41d4-a716-446655440000"
}

Behavior (as implemented):

Response (201 new enrollment, 200 idempotent replay for same user + same enrollmentRequestId):

{
  "characterName": "Somecharacter",
  "enrollmentRequestId": "550e8400-e29b-41d4-a716-446655440000"
}

(Error responses use ApiErrorDto: code, message, details — see §5 error model.)

GET /api/payouts/my-last

Purpose: pull model for Twitch Extension; return the caller’s latest winner payout as PayoutDto.

Auth: Twitch Extension JWT (Bearer) — same as GET /api/roulette/state / GET /api/pool/me.

Response:

GET /api/payouts/pending (Desktop)

Purpose: Desktop fetches payouts available for syncing/injection (winner payouts primarily Pending).

PATCH /api/payouts/{id}/status (Desktop)

Purpose: Desktop updates lifecycle state where allowed (see §3), including the InProgressPending escape hatch (streamer unlock after failed inject).

Request body (camelCase): { "status": "Pending" | "InProgress" | "Sent" | "Failed" | "Cancelled" | "Expired" } (string enum; must be an allowed transition).

Response: 200 OK with PayoutDto JSON (same fields as Shared / GET /api/payouts/my-last: id, twitchUserId, twitchDisplayName, characterName, goldAmount, enrollmentRequestId, status, createdAt, isRewardSentAnnouncedToChat).

POST /api/payouts/{id}/confirm-acceptance (Desktop) — recommended

Purpose: Record that the winner confirmed willingness to accept gold when the addon observed an in-game private message to the streamer with body matching !twgold (case-insensitive after trim; reply to the winner notification whisper; see §9) — not that mail was sent. MVP: only Desktop calls this endpoint, after [MGM_ACCEPT:UUID] in the log (§10); Twitch chat ingestion does not drive acceptance.

Trigger (MVP, normative): Desktop must call this after observing [MGM_ACCEPT:UUID] for that {id} in Logs\WoWChatLog.txt (see §10). The {id} in the URL must match the UUID in the tag.

Request (normative fields):

{
  "characterName": "Somecharacter"
}

Rules:

Mail-send confirmation → Sent (Desktop)

Purpose: Desktop must tail Logs\WoWChatLog.txt and detect [MGM_CONFIRM:UUID] (see §10). On match, call PATCH /api/payouts/{id}/status with Sent (or a dedicated confirm-mail-sent endpoint).

Rules:

POST /api/roulette/verify-candidate (Desktop)

Purpose: Submit the parsed /who result for the current spin’s candidate so the Backend can authoritatively create (or not create) a Pending payout.

Auth: X-MGM-ApiKey (Desktop).

Request (normative): same JSON object as emitted by the addon after the [MGM_WHO] prefix on the log line (§8; may include schemaVersion; Backend must accept schemaVersion: 1).

{
  "schemaVersion": 1,
  "spinCycleId": "550e8400-e29b-41d4-a716-446655440000",
  "characterName": "Norinn",
  "online": true,
  "capturedAt": "2026-04-04T12:00:01.000Z"
}

Behavior (normative):

Log alignment: Desktop must build this JSON from the [MGM_WHO] line in WoWChatLog.txt (§8, §10) and POST it; the body must match what the addon printed (same fields).

Minimum pool & roulette HTTP contract (MVP, normative)

These routes supply server-authoritative spin scheduling and pool hints for the Twitch Extension. The Extension must drive the visual roulette countdown from nextSpinAt / serverNow (poll or SSE/WebSocket later — polling is fine for MVP).

GET /api/roulette/state

Purpose: Return canonical schedule and pool size for the configured broadcaster channel (MVP: single channel per deployment).

Auth: Twitch Extension JWT (Bearer) only — no public unauthenticated access in MVP.

Response (normative fields):

{
  "nextSpinAt": "2026-04-04T12:05:00.000Z",
  "serverNow": "2026-04-04T12:01:23.456Z",
  "spinIntervalSeconds": 300,
  "poolParticipantCount": 12,
  "spinPhase": "idle",
  "currentSpinCycleId": "550e8400-e29b-41d4-a716-446655440000"
}

GET /api/pool/me

Purpose: Tell the authenticated viewer whether they are in the pool and under which CharacterName.

Auth: Twitch Extension JWT (Bearer) only; identifies TwitchUserId.

Response:

{
  "isEnrolled": true,
  "characterName": "Norinn"
}

Rules:

Extension resilience (overload / errors)

6) Persistence model (MVP, ES-first)

For MVP, the source of truth is Event Sourcing:

Outbox: Do not create an Outbox table in MVP. Helix Send Chat Message for §11 is inline try + retry after the Sent commit (see EBS — Helix §11 reward-sent delivery); not Outbox-driven. A future Outbox (e.g. Discord or other channels) may be added post-MVP when a second external side-effect class is integrated; follow Outbox as in docs/components/backend/ReadME.md (same transaction as domain events + background dispatcher) only when explicitly added to the roadmap.

Minimum recommended fields for payout read model (PayoutsReadModel):

Additional read models (pool membership, spin schedule, last spin id) are required by the roulette feature; define in implementation.

7) Expiration job (MVP)

8) Desktop → WoW injection specification (MVP)

Target process

Command format

Desktop injects /run commands that invoke addon entrypoints:

Injection strategy (MVP)

Payload chunking rule (<255 chars)

WoW chat command input has a practical limit (commonly ~255 chars). For MVP:

Roulette /who verification & unified log bridge (MVP, normative)

WoW 3.3.5a addon Lua cannot write arbitrary files to disk for Desktop consumption. All addon → Desktop signaling for /who results and payout acceptance/confirmation uses one channel: lines that appear in Logs\WoWChatLog.txt (default path below).

{
  "schemaVersion": 1,
  "spinCycleId": "550e8400-e29b-41d4-a716-446655440000",
  "characterName": "Norinn",
  "online": true,
  "capturedAt": "2026-04-04T12:00:01.000Z"
}

Example log line (single line):

[MGM_WHO]{"schemaVersion":1,"spinCycleId":"550e8400-e29b-41d4-a716-446655440000","characterName":"Norinn","online":true,"capturedAt":"2026-04-04T12:00:01.000Z"}

WoW Logs\WoWChatLog.txt path (Desktop): Default Logs\WoWChatLog.txt relative to the configured WoW install directory; full path override in Desktop settings (§10). No separate file-bridge path — only this log file for addon-originated signals in MVP.

ReceiveGold(dataString) accepts a semicolon-delimited list of payout entries:

Example:

2d2b7b2a-1111-2222-3333-444444444444:Somecharacter:10000000;

Winner notification whisper (normative)

Trigger (MVP, locked): The addon is the only component that types the §9 /whisper … into WoW. The Desktop app does not inject the Russian whisper text directly. After the Backend creates Pending for the winner, the Desktop must call /run NotifyWinnerWhisper("<payoutId>","<characterName>") (see §8) so the addon runs its Lua handler and then executes the whisper line below. Order relative to ReceiveGold: notify whisper first (this section), then mail prep/inject when the streamer syncs (ReceiveGold).

When invoked for a Pending payout, the addon must send the following exact command as a single client chat line. If the line exceeds client limits, use an addon-defined strategy that preserves the exact Russian body text:

/whisper <Winner_InGame_Nickname> Поздравляю, ты победил в розыгрыше! Дай мне своё согласие на получение награды - ответь на это сообщение одной фразой: !twgold

Whisper reply interception — acceptance (normative)

Product rules:

Mail-send detection (normative; MGM-tracked sends only)

Event: WoW 3.3.5a fires MAIL_SEND_SUCCESS when the client successfully submits an outgoing mail. MAIL_FAILED fires when the send does not complete.

MGM vs manual mail: The addon must treat MAIL_SEND_SUCCESS as the trigger for the steps below only when the send was armed by the MGM mail queue flow (e.g. after ReceiveGold / Prepare Mail from this addon). If the streamer composes and sends mail manually in the default mailbox UI without going through that armed path, the addon must not emit [MGM_CONFIRM:UUID], must not send the mail-completion whisper (below), and must not clear or advance MGM payout state for that send.

Arming (implementation guidance): Set an internal pending-send context (payout id + winner CharacterName) immediately before the SendMail(...) call that corresponds to an MGM-prepared send (e.g. secure hook on SendMail / SendMailFrame_SendMail when the recipient matches the armed payout). Clear the armed state on MAIL_FAILED, mailbox close, or successful handling of MAIL_SEND_SUCCESS.

Mail-send tag (normative; required for automated Sent)

When the addon handles MAIL_SEND_SUCCESS for an MGM-armed send, it must:

  1. Print to the default chat frame (so it appears in Logs\WoWChatLog.txt):

    • [MGM_CONFIRM:UUID]

    where UUID is the payout id.

  2. Then whisper the winner (same CharacterName as the mailed recipient) the exact in-game text (single whisper; hardcoded in addon):

    Награда отправлена тебе на почту, проверяй ящик!

    Use SendChatMessage with "WHISPER" (or equivalent 3.3.5a API) so only the winner sees this line. This is separate from [MGM_CONFIRM:UUID] (which Desktop uses for Sent).

Desktop must monitor Logs\WoWChatLog.txt for [MGM_CONFIRM:UUID] and only then transition the payout to Sent on the server (see §3, §5).

10) Chat log parsing & Desktop bridge (MVP)

Desktop WoWChatLog.txt responsibilities (normative summary)

Single integration surface for MVP: Desktop must implement one real-time tail of Logs\WoWChatLog.txt and apply three normative patterns:

Tag / prefix When emitted by addon Desktop action
[MGM_WHO]{...json} After /who parse for the spin candidate (§8) POST /api/roulette/verify-candidate with parsed JSON + X-MGM-ApiKey
[MGM_ACCEPT:UUID] After Lua detects valid whisper !twgold from the expected winner (§9) POST /api/payouts/{id}/confirm-acceptance with {id} = UUID
[MGM_CONFIRM:UUID] After MGM-armed mail succeeds (MAIL_SEND_SUCCESS, §9) PATCH payout → Sent (or equivalent)

[MGM_WHO] path (required for automated verify-candidate)

Desktop must monitor the same tail as [MGM_ACCEPT] / [MGM_CONFIRM].

WoW whisper path (acceptance — not Sent, normative)

[MGM_ACCEPT:UUID] path (required for automated confirm-acceptance)

Desktop must monitor:

Regex (normative): \\[MGM_ACCEPT:([0-9a-fA-F-]{36})\\]

Behavior notes:

[MGM_CONFIRM:UUID] path (required for Sent)

Desktop must monitor:

Regex (normative): \\[MGM_CONFIRM:([0-9a-fA-F-]{36})\\]

Behavior notes:

Note: The winner also receives an in-game whisper on MAIL_SEND_SUCCESS (MGM path only); that whisper does not replace [MGM_CONFIRM:UUID] for Desktop parsing.

11) Twitch Extension: visual roulette (MVP)