The task asked for a `RewardEvents` table plus a backfill. That would have contradicted the published promise that no viewer, member or donor information is retained after the session ends, and it would have meant owning a ledger that YouTube already keeps. So the report is DERIVED on demand instead: - `Services/SuperChatEventService.cs` — `superChatEvents.list`, parsed for both event types (a sticker must not be dropped), authorised by the existing `auth/youtube` scope, so no re-consent. - `Models/SessionRewardSummary.cs` — rolls events up for the session window with **per-currency totals that are never summed together** (a $5 and a €5 are not $10). Nothing is written down. - The YPP drawer gains a **THIS SESSION** section: counts are neutral and always shown when there were events; money appears only when the creator asks for it. `Show monetary metrics` is a drawer toggle, persisted, **off by default** — a creator who never monetized is never shown a $0.00. Also: money is labelled as estimates (removed/refunded events are excluded and totals can be restated for weeks, not days). Tests: `SessionRewardReportTests` — both reward types parse, the window makes it a session rather than a channel report, currencies stay separate, an empty session shows nothing, a sticker-only session still counts. `BackOnAirTests` moves into the serialised `MainViewModelLayoutPath` collection: it builds a MainViewModel directly and was flaking against parallel tests that swap the static layout path. FAST gate: 557 passed, 0 failed, 0 warnings.
22 KiB
Product: brand, licensing, monetization & distribution
What the product is called, what it promises, what it may never do legally, and how it ships. Read before branding, marketing, pricing, licensing or packaging work.
Brand
The UI design standard is DESIGN.md — colours, type scale, spacing, radii
and elevation, all normative, implemented by Themes/Tokens.xaml. Accent blue #38bdf8 is the only
interactive accent; red #ff5451 means live/record and nothing else. The old brand red #e94560 is
retired from app chrome (the wordmark/logo artwork is unchanged).
Marketing palette, official assets and copy live in MARCOM.md (gitignored — read it before any
branding or marketing work).
Product name vs repo/assembly branding (2026-09-14)
The product is llamacasty (llamacasty.com, llamachile.tube, llgit.llamachile.tube). The
repo path, csproj AssemblyName/RootNamespace, DB/log paths (%APPDATA%\ytLlive\...),
and most internal namings are the legacy ytLive/ytLlive — a re-brand that never renamed
the internals. Treat the two as separate: external/user-facing language says "llamacasty",
code/assembly/repo names stay ytLive. If a full internal re-brand is ever done, this note
and the UI locator (Premium/wordmark) are the checklists; it is NOT a goal pre-1.0.
Say "LlamaCasty" when reporting to the creator — never "ytLive" in prose. ytLive is
the internal repo/assembly/namespace name only. Exact current split:
| surface | name | note |
|---|---|---|
| product / site / marketing | LlamaCasty | llamacasty.com, llamachile.tube |
repo dir, csproj file, RootNamespace |
ytLive |
230+ files: namespace ytLive.* |
AssemblyName → shipped exe |
LlamaCasty |
LlamaCasty.exe — renamed 2026-11-01 |
| app data dir | %APPDATA%\ytLlive |
capital L, drifts from ytLive |
Consequences, so nobody re-hunts them:
- The assembly rename landed 2026-11-01 (before the first Steam depot, because Steamworks warns
that renaming a live exe is painful). Namespaces stay
ytLive.*— they are not user-visible and renaming them would churn every file for nothing. - An assembly-qualified pack URI spells the assembly name, so the rename hard-breaks silently
at runtime, not at compile time. Four carried it, not three:
MainWindow.xaml:14(window icon — taskbar/Alt-Tab),OverlayHost.xaml:96,RenameRecordingDialog.xaml:5, andKeepBroadcastDialog.xaml:5.ytLive.Tests/AssemblyIdentityTests.csnow guards both halves: the assembly name isLlamaCastyand no source XAML still names the legacy assembly. Packaging/Package.appxmanifest(Executable) andscripts/package-msix.shcarry the exe name too; the MSIX is unused but must not lie about the artifact.- Renaming the
%APPDATA%\ytLlivefolder is breaking — it would orphan every existing user's layout DB,Assetsand settings. Never do it casually; needs a migration. - No code compares its own assembly name (no
GetExecutingAssembly().GetName().Namechecks), so the assembly name is otherwise free to change.
AI transparency
This project is built through AI-assisted pair-programming. All code is generated under the supervision of an experienced developer with decades of coding experience. Architecture decisions, product direction, and quality gates are human-owned; AI accelerates implementation. This is a testimonial to successful human-AI collaboration, not autonomous code generation.
Licensing — proprietary since 2026-10-30
LlamaCasty is closed-source commercial software. LICENSE is the EULA; the paid tier exists
because output quality is the paid delta — the free tier is capped at a 720p30 profile (creator
ruling 2026-10-30, superseding free-forever). Prices, entitlement and channels:
commercial.md.
- Every GPL build already published stays GPL for that snapshot. Attribution/§7 terms
(
ADDITIONAL-TERMS.md) are deleted and were GPL-only; the files are gone. - No GPL third-party component may be linked. ffmpeg stays a separate subprocess (LGPL, relinkable via the shared build), OpenH264 ships as Cisco's binary, the rest are MIT/Apache/CC0. Never add a statically linked GPL library — that would make the commercial build undistributable.
- Obligations that still bind (they are third-party, not ours): keep
THIRD-PARTY-NOTICES.txtshipped and shown in About, ship full licence texts at v1, keep the ffmpeg pin immutable andlgpl-shared, never use thenonfreevariant, keep icon art CC0/public-domain, keep the ML model provenance clean (Google MediaPipe, Apache-2.0 — no model-zoo re-hosts). - Fonts: Inter + JetBrains Mono are OFL-1.1, bundled as separate files with their licence text — allowed in a commercial product, but the fonts may not be sold on their own and must not be modified under the reserved names.
- Brand artwork (name, icon, logo) is the publisher's property; the EULA grants no trademark rights and forbids implying endorsement.
- No source offer. The GPL §6 source link is gone; the repo is private.
Design Principle
This software is so intuitive that even the most right-brained person can easily intuit and use it.
Apply this to every UI decision:
- One-click go-live with working defaults
- Prefilled YouTube defaults (RTMP URL, bitrate, resolution, latency)
- Visual/drag-and-drop scene building over property panels
- Every action produces a visible outcome — no dead ends
- "We're not them" (2026-09-01): assume the creator's hardware is mediocre, because it is. Every feature spends the machine's budget once — never twice for the same result (record OR stream, not both; one reusable stream; one webcam; two mixer inputs). If a feature only sings on a high-end chassis, it doesn't ship — the OBS escape hatch is open by design.
Monetization — free tier + Pro output quality
Every feature is free and ungated (creator ruling 2026-10-08, TASK 54, kept by the 2026-10-30
freemium ruling): no feature lock, no watermark, and nothing licence-shaped in the output. The
paid delta is output quality alone — free is capped at a 720p30 profile, Pro unlocks the
higher tiers for both the stream and the recording. Prices, entitlement and channels:
commercial.md. Support is email + GitHub issues; an in-app bug reporter is out
of product.
The branding credit is retired as a paid delta and stays off. MainViewModel.ShowBrandFlash
(internal static, default false) is the master switch; BrandFlashEnabled => ShowBrandFlash && !IsPremium feeds the presenter. The code is kept, not deleted — creator ruling: "don't delete
it, just make it conditionally displayed." Entitlement must never enable it.
BrandFlashOutputTests opts in by setting the switch before constructing a MainWindow;
Credit_IsOffByDefault_EvenOnTheFreeTier guards the default.
If it is ever enabled, the mechanics that matter:
- Full-frame neon "made with LlamaCasty!" at a random position per presentation, held 2s
(500ms in / 1000ms full / 500ms out), first ~5s after start then every
rand(30s)+30s. - Published to the frame pump and the preview from the same
VideoFrame, so preview and broadcast cannot drift. Deliberately not folded intoSceneGraph.GetBakedBase— the bake cache compares element ids and would freeze the credit in place. - App-lifetime cadence: the presenter is
Start()ed once in theMainViewModelctor, not per go-live, because go-live and local recording share oneFramePump.IsPremium's setter therefore restarts the cadence timer when re-enabling (IsCadenceTimerEnabledis the test seam). - It composites into the recording and the stream (reversed 2026-09-26 — the free tier must be
honest advertising). Test arithmetic trap:
Advance(5.1)in one call blows through the 2s window (it both opens and ages the presentation) — step in 1/30s increments likeBrandFlashOutputTests. - A mid-credit downgrade cuts the advertisement on the next tick: the presenter re-checks its own
Enabledgate every frame rather than letting the credit finish.
Live prices, entitlement and channels: commercial.md. Retired pricing, the
Store IAP plan and the rejected alternatives: decisions.md.
Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1
ruling — build order: capture → report → journey → Alerts): the app is monetization-aware by
design, for every creator, free and ungated. It has three layers, all free — the capability no
competitor ships (the positioning order across features lives in MARCOM.md):
- Reward-event capture (the data foundation). The live-chat poll (
YouTubeChatService.Poll) receives every reward event overliveChat/messagesand — since TASK 43 (2026-09-24) — decodes all six types, not two:superChatEvent,superStickerEvent,newSponsorEvent,membershipGiftingEvent,giftMembershipReceivedEvent,memberMilestoneChatEventland asChatMessage.Kind(ChatEventKind) with their payload fields (display string, sticker description, gift count, gifter name, milestone months); free subscriptions emit NO event (API fact — a sub mention is a plain text row). The remaining half of the original capture plan stays open (TASK 3 item 20): persist each event to the canonical SQLiteRewardEventstable (broadcastId, type, timestamp, amountMicros, currency, tier...) +superChatEvents.list(30-day lookback) backfill — that persistence is what the session report and the journey tracker read. Alerts did not wait on it: the shipped alert path (TASK 43) renders directly from the in-memoryChatMessagefeed. - Session / broadcast report. A session is created at Go Live; the poll + End roll that session's
reward totals (new members, Super Chat
, stickers, gifts, milestones). This is the in-app replacement for the emailed external "stream activity report" — the incumbents (Streamlabs et al.) email near-useless post-stream summaries; we track the same data natively and maintain it. - Journey tracker (TASK 39, slice 1 shipped 2026-09-22). A "YPP" tab below the
Stream Settings pull-out (Live screen right edge) shows the connected creator's position
toward each YPP tier threshold. Slice 1 = current-scope data only:
channels.list?mine=true &part=statistics,contentDetails(subs/views/videos + uploads-playlist id) +playlistItems.listfor 90-day public uploads. Every refresh appends aYppSnapshotto SQLite (schema v10) — the history a later slice needs for velocity/ETA. TheauditDetailspart is OUT (fixed 2026-09-23): readding it 403s the WHOLE request — that part alone requires theyoutubepartner-channel-auditscope no app for normal creators should request (the standing flags sat unreadable since slice 1 shipped; the app'syoutube+youtube.force-sslscopes were never sufficient for them — the "no re-consent" slice-1 claim was wrong for that part). Channel standing now deep-links to the Earn page; API stats come fromstatistics,contentDetailsonly. YPP/API failures log the response body'serror.reason. YPP thresholds are versioned, date-aware DATA, never constants — the Tier-2 bar doubles for new applicants on 2027-02-01 (long-form 4,000 → 8,000 qualified hrs / 365d; Shorts 10M → 20M / 90d), while the Tier-1 fan-funding bar (500 subs / 3,000 hrs / 3M Shorts) is unchanged. Hardcoding the pre-2027 numbers would ship the wrong ETA. Watch-hours / Shorts / velocity / ETA are slice 2: Analytics-API scopes (yt-analytics.readonly, additiveyt-analytics-monetary.readonly) behind theIChannelStatsProviderseam (additive capability, requires an OAuth re-consent). Compliance items no API exposes (2FA, AdSense linkage) are in-app self-reported checkboxes with deep links; channel standing (exposed only via the partner-audit scope) deep-links to the Earn page. Slice 2's once-daily refresh gate + snapshot history were baked into slice 1.
Auth gates Go Live, but not exploration
The app is fully usable without authentication: users can build scenes, add sources, compose previews, and audition the software with zero commitment. But going live requires authentication — it's the one capability gated behind YouTube sign-in. The sign-in should never pressure the user ("sign in (optional)", not a modal wall): the two-state top bar shows Start Stream (offline) / End Stream (live), and the Start Stream dialog hosts the account — a saved session appears as the default with "Change Account"; with none saved, a "Sign in to YouTube" button starts OAuth and the Start button stays disabled until signed in.
Account assumption (do not build an account setup flow)
Connecting uses Google OAuth ("Sign in with Google") to link an existing YouTube creator account. ytLlive never creates or sets up accounts — that is YouTube's job. If the creator has no YouTube channel, they go to YouTube first. This assumption is explicit and must never be silently replaced by an in-app account-creation step. Zero state = the Start Stream dialog's "Sign in to YouTube" button; going live is unreachable until an account is connected.
Windows packaging & distribution (Steam only)
Live channel policy, the artifact and costs: commercial.md. Build and depot
mechanics: Distribution.md. Retired routes and cause of death:
decisions.md.
One vector: Steam. One paid app plus its associated free Demo. They use the same code; the Demo checks ownership of the paid app to unlock higher output quality. No direct download, no GitHub binary, no winget, no Microsoft Store, and no subscription.
Consequences that are architecture, not packaging detail:
- ⛔ There is no self-updater, and now no version banner either. Steam delivers updates; the
app keeps nothing that downloads or executes, for the same Store-policy reason it always had.
ytLive.Tests/StorePackagingTests.csmust keep asserting that no self-updater returns. - ⛔
steam_api64.dllis a functional dependency, not the rejected "native helper DLL for IP protection" — Steam app ownership cannot be read without it. The app must start without Steam running and fall back to the free profile. - The MSIX package stays buildable but is now unused —
scripts/package-msix.sh(TASK 48 slice 2) still producesartifacts/LlamaCasty.msixfromPackaging/Package.appxmanifest+Packaging/Assets/. It was the Store channel's artifact; with Steam exclusive there is no submission target. The full-trustpackagedClassicAppfindings below remain true and are still worth reading before any packaged build is attempted. - ⛔ Bundle ffmpeg — no runtime downloads (was Store policy 10.2.2; still right for a Steam
depot, and it fixes the 2026-09-01 404). See
output.md→ "FFmpeg locator". - ⛔ 64-bit Windows only (creator ruling 2026-10-09). Both csproj files set
<PlatformTarget>x64</PlatformTarget>(the test project too — an AnyCPU test project referencing an x64 app raises MSB3270 and breaks the 0-warning gate). Why: the bundled ffmpeg is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a 32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway. Public copy says "64-bit Windows 10 or 11". - Portable mode is a feature, not a channel:
portable.txtbeside the exe keeps the layout DB and settings in.\data\(hung offLayoutPathOverride), so a copied install carries someone's scenes. Steam installs it; the marker is for afterwards.
What does NOT change: no watermark and no feature gating. The free tier is the whole app at a
720p30 profile. Monetization above governs.
⛔ Durable invariant: full trust, or recording silently breaks
MSIX packages run at one of two trust levels. A packaged WPF app declaring
rescap:Capability Name="runFullTrust" runs at mediumIL — the same permissions as a
standard desktop app, NOT AppContainer — and may spawn child processes, which is why
bundling ffmpeg is legal. Writes to user-profile locations (AppData, Downloads, MyVideos)
pass through unvirtualized at mediumIL; the package install folder is read-only.
ViewModels/MainViewModel.Recording.cs writes recordings to
%USERPROFILE%\Downloads / SpecialFolder.MyVideos (DefaultRecordFolder(),
ChooseRecordFolder() via Microsoft.Win32.OpenFolderDialog), and the whole
temp-write-then-move lifecycle stays in one directory. That works only because we are full
trust. No broadFileSystemAccess capability is needed, and none should be added.
⚠️ If anyone flips the manifest to appContainer, Directory.CreateDirectory and
File.Move start resolving to a per-package virtualized location and output vanishes with no
error. A comment at DefaultRecordFolder() is owed when the manifest lands (TASK 48
item 6) — not before; a comment describing a manifest that doesn't exist is worse than none.
Signing — not needed, because Steam installs the app
Steam's client delivers the files, so there is no browser download, no Mark-of-the-Web and no SmartScreen wall. The certificate has no job in this channel. It stays on the shelf for the day a non-Steam download exists:
- Azure Artifact Signing (renamed from "Trusted Signing") — $9.99/month Basic, up to 5,000 signatures, requires a paid Azure subscription; individual identity validation is the slow part and must be renewed.
- Never EV — Microsoft removed the EV SmartScreen instant-bypass in March 2024, so EV buys what OV buys. Never a certificate with an HSM for this product.
- Steam does not sign the exe. A UAC prompt for the app is elevation, not publisher trust.
Distribution and licensing are independent — the channel never decides billing
Steam distributes the paid app and its associated free demo. They use the same code; the demo asks Steam whether the current account owns the paid app, and the answer controls access to higher output quality. The paid app is a one-time purchase: $79 during the announced launch period, then $99 on the announced date. The app's EULA must expressly allow commercial and monetized streaming use.
⛔ "Only when the user says so" is enforced by Windows, not by us
Camera/mic consent is layered: Store policy 10.6 forbids circumventing OS checks; the Windows desktop-app camera/microphone toggle (Settings → Privacy & security, since Win10 1903) is the gate; hardware lights and tray indicators are the OS's disclosure mechanism. So route everything through the standard MF/DShow/WASAPI paths and we never write that certification ourselves — the OS does it and gets neither our credit nor our blame.
⚠️ Untested: whether that toggle reliably gates a DShow webcam and a capture card.
Microsoft's own support page admits desktop apps "might still be able to access your camera
or microphone even when these settings are turned off." This is not a certification
failure (we declare webcam honestly and use supported APIs) but it is a real user-trust
issue — so no privacy-forward marketing copy until it is measured. Guard note added to
MARCOM.md.
⛔ Durable invariant: chat is rendered, never stored
TASK 49's profanity filter is local, on-device, opt-in, non-persistent, with a
user-supplied word list — never a hardcoded slur list baked into the binary (unpleasant
artifact, false positives across dialects, extractable). It must never match the SuperChat
amount or any reward field — those are financial data under policy 10.5.5.
The non-persistence half is not just hygiene: it is the load-bearing half of the 11.12 UGC certification answer (mirrored YouTube chat is moderated at industrial scale upstream; ~1.6B comments removed in Q1 2026 alone, child safety the #2 reason at 124.6M). No future feature — chat history, moderation logs, analytics, crash payloads — may quietly break that assumption. If one is ever proposed, the 11.12 position must be re-argued first.
Monetization: derived, never a ledger (2026-11-05)
YouTube is the sole authority for monetization progress and qualification; the API exposes no
eligibility field (channels.list has no monetization part; auditDetails needs an MCN scope). So the app
asks YouTube on demand (superChatEvents.list for the session report; Analytics for the YPP bars in
TASK 39·2) and stores nothing. That keeps the promise above literally true — there is no reward ledger
to retain — and it means the app never has to model YouTube's moving thresholds as our own data.
Money is off by default ("Show monetary metrics", in the YPP drawer, persisted): counts are neutral, and a creator who never monetized is never shown a zero. Amounts are estimates that can be restated for weeks, and are labelled as such wherever they appear.
Minors
The operator is 13+ by construction, not by promise: LlamaCasty requires a YouTube account to stream, and YouTube's ToS sets the account floor at 13 (regional variants reach 14; 13–17 need parent/guardian permission; under-13s are on supervised/YouTube Kids accounts). So a COPPA theory premised on collecting PI from under-13s has no purchase against the operator. The residual is viewers — still possible on supervised family accounts — and it is weak: live chat is restricted there, YouTube moderates before we see it, and we persist nothing. COPPA is very unlikely to bind (not child-directed), but note the enforcement trend (FTC Sept 2025 Apitor: operators are responsible for what third-party components collect) — so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no persisted chat or reward payloads."