diff --git a/Models/ScoreboardCadence.cs b/Models/ScoreboardCadence.cs
new file mode 100644
index 0000000..781db88
--- /dev/null
+++ b/Models/ScoreboardCadence.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace ytLive.Models;
+
+/// What kind of session the app is in, for polling decisions.
+public enum SessionPhase
+{
+ Idle,
+ Live,
+ Test,
+ Recording,
+}
+
+/// Which channel figures a poll should fetch.
+public enum ScoreboardPoll
+{
+ None,
+ SubscribersOnly,
+ Both,
+}
+
+/// Polling policy for the scoreboard's two channel figures (creator ruling 2026-10-30).
+///
+/// Analytics answers in DAYS, and test/record sessions have no public audience — so querying
+/// watch hours then is pure waste. Subscriber count is cheap (one quota unit) and moves during any
+/// session, so it polls whenever the app is working. Idle means silent.
+public static class ScoreboardCadence
+{
+ public static readonly TimeSpan AnalyticsInterval = TimeSpan.FromMinutes(30);
+ public static readonly TimeSpan SubscriberInterval = TimeSpan.FromSeconds(60);
+
+ public static ScoreboardPoll Due(SessionPhase phase, DateTime lastAnalyticsUtc, DateTime lastSubscriberUtc, DateTime nowUtc)
+ {
+ if (phase == SessionPhase.Idle) return ScoreboardPoll.None;
+
+ var analyticsDue = phase == SessionPhase.Live && nowUtc - lastAnalyticsUtc >= AnalyticsInterval;
+ var subscribersDue = nowUtc - lastSubscriberUtc >= SubscriberInterval;
+
+ return (analyticsDue, subscribersDue) switch
+ {
+ (true, true) => ScoreboardPoll.Both,
+ (true, false) => ScoreboardPoll.None, // analytics alone is not a poll kind
+ (false, true) => ScoreboardPoll.SubscribersOnly,
+ _ => ScoreboardPoll.None,
+ };
+ }
+}
diff --git a/ytLive.Tests/ScoreboardCadenceTests.cs b/ytLive.Tests/ScoreboardCadenceTests.cs
new file mode 100644
index 0000000..cc51ba7
--- /dev/null
+++ b/ytLive.Tests/ScoreboardCadenceTests.cs
@@ -0,0 +1,39 @@
+using System;
+using Xunit;
+using ytLive.Models;
+
+namespace ytLive.Tests;
+
+/// The quiet-by-default rule: watch hours only during a live stream (Analytics is a daily
+/// figure and test/record sessions have no public audience), subscribers whenever the app is
+/// working, and silence when idle.
+public class ScoreboardCadenceTests
+{
+ [Fact]
+ public void Analytics_polls_only_when_live_and_subscribers_poll_during_any_session()
+ {
+ var now = new DateTime(2026, 10, 30, 12, 0, 0, DateTimeKind.Utc);
+ var old = now.AddHours(-2);
+
+ // Idle: silent, even if everything is stale.
+ Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Idle, old, old, now));
+
+ // Live with stale figures: both.
+ Assert.Equal(ScoreboardPoll.Both, ScoreboardCadence.Due(SessionPhase.Live, old, old, now));
+
+ // Test / recording: subscribers only — no public audience, so no analytics.
+ Assert.Equal(ScoreboardPoll.SubscribersOnly, ScoreboardCadence.Due(SessionPhase.Test, old, old, now));
+ Assert.Equal(ScoreboardPoll.SubscribersOnly, ScoreboardCadence.Due(SessionPhase.Recording, old, old, now));
+
+ // Fresh figures: nothing is due.
+ Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Live, now, now, now));
+
+ // Analytics alone never polls (it is not a kind the poller knows).
+ Assert.Equal(ScoreboardPoll.None,
+ ScoreboardCadence.Due(SessionPhase.Live, old, now.AddSeconds(-10), now));
+
+ // Subscribers alone mid-window.
+ Assert.Equal(ScoreboardPoll.SubscribersOnly,
+ ScoreboardCadence.Due(SessionPhase.Live, now, old, now));
+ }
+}