!twgift) — streamer-scoped serialized flowThe !twgift <CharacterName> flow is a separate queue from payout roulette and is persisted in Marten/PostgreSQL.
streamerId + viewerId).StreamerId; only one active request can be in SelectingItem/WaitingConfirmation at a time.Pending; backend auto-promotes oldest pending request when no active request exists.max(0, queuePosition - 1) * 65 seconds.Pending -> SelectingItem -> WaitingConfirmation -> Completed
Failed is reachable from any non-terminal state (timeout, no items, explicit failure).SelectingItem: 60 seconds.WaitingConfirmation: 5 minutes.Failed, then promotes next pending request.GET /api/streamers/{streamerId}/gift-queue (viewer JWT): queue snapshot and current position.POST /api/gift-requests (viewer JWT): create gift request.PATCH /api/gift-requests/{id} (Desktop API key): state update.POST /api/gift-requests/{id}/select-item (Desktop API key): report selected inventory item.POST /api/gift-requests/{id}/confirm (Desktop API key): finalize confirmed send/decline.MGM_RequestGiftItems(giftRequestId) and logs item payload marker [MGM_ITEMS:<uuid>]....[MGM_GIFT_ACCEPT:<uuid>] when recipient confirms with whisper !twgift.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.
MimironsGoldOMatic.Backend.Api (ASP.NET Core host) and Backend.* projects — the server surface that implements Extension-facing APIs, Twitch Extension JWT validation (§5.1, Bearer), and integrations that require broadcaster/Twitch credentials. (The legacy monolith folder MimironsGoldOMatic.Backend was removed; Backend.Api is the runtime host.)Send Chat Message (§11 reward-sent line) and other Helix calls tied to this product.channel.chat.message events for !twgold <CharacterName> enrollment (§1 glossary, §5). Chat messages do not POST directly to the EBS HTTP surface; EventSub is the transport from Twitch to the EBS.!twgold pool enrollment, the EBS must trust the subscriber / badges data supplied on the channel.chat.message EventSub notification only. Do not perform secondary Helix lookups to verify subscription during enrollment (saves API quota; Twitch is authoritative for the event payload).Sent, the EBS must call Helix Send Chat Message with the §11 template (3 attempts, simple retry policy — e.g. Polly or equivalent).Sent (in-game gold is already committed; chat is best-effort). Log the failure at error level for operations.PayoutId — e.g. boolean IsRewardSentAnnouncedToChat on the payout read model / entity, or trigger Helix only on transition into Sent from a non-terminal state (idempotent PATCH must not re-post).!twgold chat commands for this product. MVP (locked): at enrollment, use the subscriber indication from the channel.chat.message EventSub payload only — no secondary Helix subscriber lookups for enrollment (see EBS section above, §5).!twgold <CharacterName>) only. MVP: use EventSub channel.chat.message (required). IRC/bot-only paths are not part of the MVP contract unless explicitly added later.!twgold <CharacterName> typed in broadcast Twitch chat, where CharacterName is the viewer’s server / in-game nickname used for the roulette pool. The !twgold prefix is case-insensitive (e.g. !TWGold Name counts); whitespace-separated; CharacterName must satisfy the same validation rules as CharacterName elsewhere in this spec.CharacterName may appear at most once among active pool entries (no two different viewers holding the same name in the pool simultaneously). A second viewer (or the same viewer) attempting to enroll the same name while it is already taken must receive a clear error (e.g. character_name_taken_in_pool).TwitchUserId already has a pool row and sends a new !twgold <OtherName>, replace that row’s CharacterName with the new name (release the old name for other viewers), subject to validation and uniqueness of OtherName.!twgold <CharacterName> and not been removed per pool rules. Non-winners of a spin remain in the pool. Winners are removed from the pool when their payout becomes Sent (gold mail confirmed — see §3, §5); they may re-enter the pool by sending !twgold <CharacterName> again in broadcast chat.GET /api/roulette/state); the Extension must display the roulette countdown using API-provided timestamps (not a free-running client-only clock as the source of truth).System.Random or another suitable RNG; document the choice in Backend code/readme./who): the roulette must ensure the candidate winner is actually in-game and online by running /who <Winner_InGame_Nickname> where the name matches the pool CharacterName. MVP normative split: the addon runs /who, parses the 3.3.5a result, and emits a [MGM_WHO] log line (§8, §10) via DEFAULT_CHAT_FRAME:AddMessage (or equivalent) so it appears in Logs\WoWChatLog.txt — WoW 3.3.5a addons cannot write arbitrary files; there is no JSON file-bridge. Desktop tails the same log, parses [MGM_WHO], and POSTs the payload to POST /api/roulette/verify-candidate. The Backend is authoritative: it only creates a Pending payout when it accepts an online: true report for the current spin cycle (subject to §5 grace window). No re-draw in the same 5-minute cycle: if the candidate is offline (or a single-person pool is offline), no winner is produced until the next scheduled spin.Winner notification whisper (WoW, normative): After a Pending payout exists for the winner, the addon must cause the client to send an in-game whisper to the winner’s character (<Winner_InGame_Nickname> = enrolled CharacterName) using this exact chat command text (single line; if the line exceeds client limits, use an addon-defined strategy that preserves the exact Russian body text):
/whisper <Winner_InGame_Nickname> Поздравляю, ты победил в розыгрыше! Дай мне своё согласие на получение награды - ответь на это сообщение одной фразой: !twgold
Implementation note: If the client uses /w instead of /whisper, document equivalent behavior; do not change the Russian body text without a product decision.
nextSpinAt is aligned to UTC wall-clock boundaries :00, :05, :10, … (multiples of 5 minutes).Pending or InProgress.Sent, Failed, Cancelled, or Expired.!twgold with case-insensitive comparison after trim (no other words or punctuation). The addon must detect this in Lua whisper events, then print [MGM_ACCEPT:UUID] to WoW chat so it appears in Logs\WoWChatLog.txt; the Desktop utility must tail that file and call POST .../confirm-acceptance (see §9–10). This is not proof that mail was sent (see [MGM_CONFIRM:UUID]). Non-normative: Twitch broadcast chat !twgold (no args) as an alternate acceptance path is not part of the MVP contract unless explicitly added later.[MGM_ACCEPT:UUID]): Addon-emitted line printed to WoW chat after a valid whisper !twgold is observed; UUID is the payout id. Used so Desktop can automate confirm-acceptance via the same WoWChatLog.txt watcher as [MGM_CONFIRM:UUID] (different regex). Not the same as parsing the user’s whisper text from the log — the addon owns the tag content.[MGM_CONFIRM:UUID]): after an MGM-armed in-game mail send succeeds (MAIL_SEND_SUCCESS, §9), the addon must print [MGM_CONFIRM:UUID] to WoW chat so it appears in Logs\WoWChatLog.txt, then whisper the winner the mail-completion Russian line (§9). Manual non-MGM mail sends must not emit this tag. Desktop must parse [MGM_CONFIRM:UUID] (required); Sent on the server is driven by this signal (see §10). Sent also removes the winner from the participant pool (see §5).TwitchUserId / enrollment idempotency as in §4).TwitchUserId.TwitchUserId at a time (same as before; applies once a viewer becomes a spin winner and a payout record exists). A second win/spin finalize while a non-terminal payout exists must be rejected with active_payout_exists (or equivalent); do not auto-expire or replace an existing active payout to make room.user_id claim) or client IP; POST /api/twitch/eventsub is not rate-limited (separate partition with no limiter).Pending: created for the selected winner after a spin, not yet synced/injected by Desktop.InProgress: explicitly claimed by Desktop when streamer clicks Sync/Inject (prepares mail / queue). Desktop must only perform this transition when the WoW client target is detected (MVP: foreground WoW.exe per Desktop → WoW injection, §8); do not move to InProgress if WoW is not found.Sent: confirmed on the server when the Desktop utility observes [MGM_CONFIRM:UUID] for that payout id in Logs\WoWChatLog.txt (required mail-send confirmation). WoW whisper reply !twgold (case-insensitive) records willingness to accept earlier in the flow and does not replace [MGM_CONFIRM:UUID]. Transitioning to Sent removes that winner from the participant pool (they may re-enroll via !twgold <CharacterName> in Twitch chat).Failed: streamer/Desktop marked failure (e.g., faction restriction, injection failure, etc.).Cancelled: streamer cancelled in Desktop.Expired: auto-closed by backend when older than 24 hours (terminal).| 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 |
TwitchUserId (from Twitch identity; numeric string is acceptable for storage).TwitchDisplayName (for Desktop / overlay UX).CharacterName (single realm assumption; faction failures handled manually by streamer).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).
message_id (or equivalent) for the same !twgold <CharacterName> message; ignore duplicate chat deliveries.EnrollmentRequestId is a unique identifier for a single POST /api/payouts/claim (client-generated UUID recommended) when that path is used.CharacterName among active pool rows (see glossary).MVP behavior on duplicate EnrollmentRequestId (Extension only):
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.
X-MGM-ApiKey: <value>
POST /api/twitch/eventsubTwitch-Eventsub-Message-Id, Twitch-Eventsub-Message-Timestamp, and Twitch-Eventsub-Message-Signature with HMAC-SHA256 when Twitch:EventSubSecret is configured; if the secret is empty, verification is skipped (convenient for local tunnel testing — do not use an empty secret in production).challenge with plain-text body for subscription verification. For channel.chat.message notifications, the EBS parses the payload and calls the same enrollment rules as chat (!twgold <CharacterName>, subscriber badges from payload: subscriber, founder, premium), with message_id deduplication.Program.cs so Twitch deliveries are not throttled as viewer traffic.POST /api/payouts/claim vs chat enrollmentEventSub): Subscriber gating uses only the EventSub payload (§1, EBS section). There is no DevSkipSubscriberCheck branch on this path.POST /api/payouts/claim): The handler enforces Mgm:DevSkipSubscriberCheck. When false (default), the API returns 403 with not_subscriber — Helix-based subscriber verification for this path is not implemented yet; set DevSkipSubscriberCheck to true in Development only to exercise the claim API (e.g. Dev Rig). Product intent: eventual Helix verification for claim should match chat enrollment rules.!twgold <CharacterName> in broadcast Twitch chat (prefix case-insensitive), and CharacterName is valid and not already held by another active pool entry, add or update that viewer’s pool row (§1 glossary: same TwitchUserId may replace their previous CharacterName). No payout is created at this step. MVP: subscriber eligibility comes from the channel.chat.message EventSub payload only (no Helix lookup). If they are not a subscriber per that payload: do not add to pool; log server-side only (no chat bot reply required). If the name is taken by another viewer, reject (character_name_taken_in_pool or equivalent).Pending payout and winner notification whisper (§9), the winner must reply in-game with !twgold (case-insensitive; §9). The addon forwards to Desktop → POST .../confirm-acceptance. The streamer should send in-game mail only after acceptance is recorded (§9).POST /api/payouts/claim (§5) may still add a subscriber to the pool for Dev Rig / Extension-only flows; behavior must match chat enrollment rules (subscription + unique CharacterName).Sent, remove that participant (TwitchUserId + CharacterName) from the pool. They may join again with a new !twgold <CharacterName> message in chat.Online check (required): For each spin cycle, the Backend selects a candidate from the pool, then requires an online: true /who report delivered via [MGM_WHO] in Logs\WoWChatLog.txt → Desktop tail → POST /api/roulette/verify-candidate (§8, §10). If the report is online: false (or missing before the cycle boundary + grace window), no Pending payout is created for that cycle — no re-draw in the same 5-minute window. If the pool had exactly one participant and they are offline, no winner this cycle (same rule).
online: true report for the candidate, it creates a payout record in Pending for that winner’s CharacterName.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:AddMessage → WoWChatLog.txt → Desktop (§8, §10).
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):
duplicate_enrollmentactive_payout_existslifetime_cap_reachedinvalid_character_nameunauthorizedforbidden_apikeyterminal_status_change_not_allowednot_foundpool_empty (if a spin is requested with zero participants — should not occur if minimum is 1 and spin is only scheduled when valid)character_name_taken_in_pool (enrollment command uses a name already active in the pool)not_subscriber (chat or API enrollment from non-subscriber)| 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.
/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):
Mgm:DevSkipSubscriberCheck is false, the EBS returns 403 with not_subscriber (Helix verification for this path not wired yet). When true (local dev only), the check is skipped so Dev Rig / tests can call the endpoint.characterName (FluentValidation / CharacterNameRules) and pool uniqueness of the name; enforce one active payout per user, lifetime cap, and EnrollmentRequestId idempotency (same as §4).Pending payout from this call; only spin winner yields a payout row.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.)
/api/payouts/my-lastPurpose: 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:
200 OK with a PayoutDto-shaped JSON body when a winner payout exists for TwitchUserId.404 Not Found when the viewer has no winner payout row yet (strict “payout-only” contract; pool-only membership is not returned here — use GET /api/pool/me)./api/payouts/pending (Desktop)Purpose: Desktop fetches payouts available for syncing/injection (winner payouts primarily Pending).
/api/payouts/{id}/status (Desktop)Purpose: Desktop updates lifecycle state where allowed (see §3), including the InProgress → Pending 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).
/api/payouts/{id}/confirm-acceptance (Desktop) — recommendedPurpose: 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"
}
characterName is required (must match the payout’s winner character for validation).Rules:
X-MGM-ApiKey.WinnerAcceptedWillingToReceiveAt); does not set Sent.[MGM_ACCEPT:UUID] line (log replay) must not create inconsistent state; return success if already accepted.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:
X-MGM-ApiKey.[MGM_CONFIRM:UUID] is required for automated Sent; it proves the addon reported mail was sent./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):
online is true and spinCycleId matches the active spin cycle and characterName matches the server-selected candidate, the Backend creates the Pending payout for that viewer.online is false, the Backend does not create a payout; no re-draw occurs in the same cycle (§1 glossary).400 with a stable error code.:00/:05/:10/… spin boundary that closes eligibility for verify-candidate for the active spinCycleId (the instant the cycle stops waiting for /who / Desktop). If the POST arrives within 30 seconds after that boundary instant, the Backend may still accept a valid report (tolerance for client/log delay). Beyond that window, treat as out of sequence (400) unless the Backend defines a broader policy in a later revision.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).
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).
/api/roulette/statePurpose: 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"
}
nextSpinAt: ISO-8601 UTC instant of the next scheduled spin boundary (:00, :05, :10, … UTC — multiples of 5 minutes; single source of truth for countdown UI).serverNow: ISO-8601 UTC “now” on the server (helps correct client drift when computing remaining time).spinIntervalSeconds: 300 in MVP.poolParticipantCount: non-negative integer; number of active pool entries for the current channel.spinPhase: closed enum for MVP — exactly one of: idle, collecting, spinning, verification, completed. Transitions between phases for a cycle are Backend-defined (implementation detail), as long as responses remain consistent with this contract and docs/components/twitch-extension/UI_SPEC.md UX.currentSpinCycleId: UUID string for the active spin cycle (omit or null when spinPhase is idle); used to correlate POST /api/roulette/verify-candidate and [MGM_WHO] log payloads./api/pool/mePurpose: 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"
}
isEnrolled = false and characterName may be null or omitted.Rules:
nextSpinAt / serverNow are the only authoritative schedule for “time until next spin”; the Extension must still display the roulette timer/countdown in the UI (see §11).429, 503, or network failure when polling GET /api/roulette/state, GET /api/pool/me, or GET /api/payouts/my-last, the Extension should show a friendly error (see docs/components/twitch-extension/UI_SPEC.md) and exponential backoff between retries (cap the maximum interval, e.g. ≤ 60s), plus a Retry control so the viewer is not stuck in a tight loop. 503 indicates temporary overload or dependency failure; behavior is host-defined beyond this client guidance (§5 error model).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):
Id (UUID, PK)TwitchUserId (string/varchar; indexed)TwitchDisplayName (string/varchar)CharacterName (string/varchar)GoldAmount (bigint/int64; always 1000 in MVP)EnrollmentRequestId (string/varchar; UNIQUE where applicable for pool enrollment correlation)Status (enum/string; indexed)CreatedAt (timestamp; indexed with status for expiration sweep)UpdatedAt (timestamp)WinnerAcceptedWillingToReceiveAt (timestamp nullable): set when Desktop reports !twgold for this payout (acceptance to receive gold).IsRewardSentAnnouncedToChat (boolean): set true only after Helix Send Chat Message succeeds for §11; while false, transitioning to Sent may trigger the inline retry loop; once true, do not call Helix again for that PayoutId (idempotent PATCH). If all 3 attempts fail, leave false, log, and do not rollback Sent (see EBS — Helix §11 reward-sent delivery).Additional read models (pool membership, spin schedule, last spin id) are required by the roulette feature; define in implementation.
Pending or InProgress to Expired when CreatedAt < now - 24h.Expired is terminal and MUST NOT be reactivated.WoW.exe process in MVP.Desktop injects /run commands that invoke addon entrypoints:
/run ReceiveGold("<payload>") — mail-queue payload (winner payouts for InProgress mail flow)./run NotifyWinnerWhisper("<payoutId>","<characterName>") — after the Backend creates Pending for that winner, Desktop must inject this line so the addon sends the §9 winner notification whisper (addon issues /whisper; Desktop does not paste the Russian text). payoutId is the payout UUID; characterName matches pool CharacterName. Respect §8 <255 chars per injected line (this call is short).PostMessage.SendInput (operator-switchable in Desktop settings) when primary injection is blocked/unreliable.WoW chat command input has a practical limit (commonly ~255 chars). For MVP:
ReceiveGold(...) call./run ReceiveGold("...") lines if needed./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).
/who <CharacterName> in the client, parses online/offline for 3.3.5a, then emits one line to the default chat frame via DEFAULT_CHAT_FRAME:AddMessage (or equivalent) so the client records it in WoWChatLog.txt.[MGM_WHO] immediately followed by a single JSON object (UTF-8) on the same line — no newlines inside the object. The JSON fields must match POST /api/roulette/verify-candidate (§5). Plain language: “JSON” here means structured text in braces { … } with named fields ("schemaVersion", "spinCycleId", etc.) and values; it must be one continuous line in WoWChatLog.txt. Extra spaces are optional; compact output (see example below) is recommended.{
"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"}
schemaVersion: 1 for MVP.spinCycleId: must match currentSpinCycleId from GET /api/roulette/state for the active cycle.capturedAt: ISO-8601 UTC when the addon determined the result.Logs\WoWChatLog.txt, parses [MGM_WHO] lines, and POSTs the JSON to POST /api/roulette/verify-candidate with X-MGM-ApiKey. The Backend is authoritative for creating Pending (see §5).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.
!twgold, mail queue, and mail-send tag (MVP)ReceiveGold(dataString) accepts a semicolon-delimited list of payout entries:
UUID:CharacterName:GoldCopper;
UUID: payout idCharacterName: WoW character name (MVP validation should prevent : and ;)GoldCopper: integer copper amount (MVP: 1000g = 10000000 copper)Example:
2d2b7b2a-1111-2222-3333-444444444444:Somecharacter:10000000;
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
<Winner_InGame_Nickname> is the enrolled CharacterName for that payout.!twgold only (comparison case-insensitive after trim).CharacterName for the active Pending payout) and the message text matches !twgold (case-insensitive after trim), the addon must print [MGM_ACCEPT:UUID] to WoW chat (same channel/window behavior you use for [MGM_CONFIRM:UUID] so it is captured in Logs\WoWChatLog.txt). UUID is the payout id.WoWChatLog.txt). HTTP from Lua is not used.Product rules:
!twgold (consent).!twgold reply does not mean mail was sent; Sent still requires [MGM_CONFIRM:UUID] in the WoW log.Logs\WoWChatLog.txt and calls POST .../confirm-acceptance when [MGM_ACCEPT:UUID] matches (§5, §10).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.
Sent)When the addon handles MAIL_SEND_SUCCESS for an MGM-armed send, it must:
Print to the default chat frame (so it appears in Logs\WoWChatLog.txt):
[MGM_CONFIRM:UUID]where UUID is the payout id.
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).
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_ACCEPT:UUID] is addon-emitted after whisper events — Desktop is not parsing the user’s whisper body from the log. (Parsing raw whisper lines from WoWChatLog.txt remains out of scope for MVP.)[MGM_WHO] path (required for automated verify-candidate)Desktop must monitor the same tail as [MGM_ACCEPT] / [MGM_CONFIRM].
[MGM_WHO] immediately followed by JSON (§8).schemaVersion; POST to POST /api/roulette/verify-candidate.400 for out-of-sequence reports.Sent, normative)!twgold as a private in-game message from the winner is delivered to the addon via Lua; the addon then prints [MGM_ACCEPT:UUID] so Desktop can complete the HTTP step (see §9).[MGM_ACCEPT:UUID] path (required for automated confirm-acceptance)Desktop must monitor:
Logs\WoWChatLog.txtRegex (normative): \\[MGM_ACCEPT:([0-9a-fA-F-]{36})\\]
Behavior notes:
POST /api/payouts/{id}/confirm-acceptance (see §5).{id} does not exist: log and ignore (do not spam the API) — §5 error handling may still return 404 if called.[MGM_CONFIRM:UUID] path (required for Sent)Desktop must monitor:
Logs\WoWChatLog.txt (same tail as above)Regex (normative): \\[MGM_CONFIRM:([0-9a-fA-F-]{36})\\]
Behavior notes:
Sent for that payout id.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.
GET /api/roulette/state (nextSpinAt, serverNow); do not use a client-only clock as the authority for spin timing. Fixed 5-minute cadence; no early spin UX.!twgold <CharacterName> in stream chat to join the pool (not only a form inside the Extension). Character names in the pool must be unique (explain collision if needed)./who <Winner_InGame_Nickname> before the win is final (see §5); the Extension may reflect “checking…” / “verified” state if the Backend exposes it via spinPhase / API fields.Pending payout if applicable).!twgold (case-insensitive) to consent; then the streamer sends gold mail; Sent follows [MGM_CONFIRM:UUID] in WoWChatLog.txt.Sent, the winner is removed from the pool; they can re-enter with !twgold <CharacterName> in chat again.Twitch chat — reward sent announcement (normative copy): When a winning payout becomes Sent (gold mail confirmed), the EBS MUST attempt to post the confirmation line to broadcast stream chat if Helix is available — best-effort after 3 inline retries (see EBS — Helix §11 reward-sent delivery; failure to post does not undo Sent). The exact template is ( WINNER_NAME = enrolled CharacterName for that payout):
Награда отправлена персонажу <WINNER_NAME> на почту, проверяй ящик!
Extension: keep this string hardcoded in the Twitch Extension source (e.g. a small template helper) so in-panel copy stays aligned with the same template the EBS posts to broadcast chat.
Delivery (MVP, locked): The EBS must invoke Twitch Helix Send Chat Message immediately after the authoritative Sent commit, with 3 retry attempts. Use the template above with CharacterName from the payout. At-most-once per PayoutId (see IsRewardSentAnnouncedToChat, §6). The Extension does not trigger chat delivery; it may only reflect Sent in UI via existing read APIs. The chat line must not rely on the WoW addon (Twitch chat is outside the game client).
Winner panel: the winning viewer’s Extension UI must show equivalent confirmation (can reuse the same Russian template with WINNER_NAME = self).
429, 503, network errors), follow §5.1 Extension resilience (backoff + Retry).