Daily Streak Checklist

Fires 3x/day to Discord: a morning digest, then afternoon/evening reminders that only mention what's still outstanding (silent if everything's already done — no nagging for the sake of it). The morning ping also includes a live "All quests" snapshot for MS Rewards and Xbox (see below) — informational only, not tied to streaks.

Status of each tracked item

  • Brilliant Daily — ✅ auto-verified, via a real headless browser (Kernel) reading your actual rendered streak number.
  • MS Rewards — Bing daily set — ✅ auto-verified, via the same Kernel approach, reading the real dashboard's "Activity: N/N" text.
  • MS Rewards — Xbox / bonus activities — ⚠️ not a streak item at all (removed from ITEMS 2026-09-12). Quests rotate (daily/weekly/monthly) rather than being a clean "done once a day" thing, so there was never a real boolean to verify — checkMsRewardsXbox (still in lib/checkers/msRewards.ts) always returned { done: null }, which permanently stuck both this item and the combined "full day" streak at zero. Rather than show a checklist line and a streak number that could never move, it's just not in ITEMS any more. Xbox is still covered properly — every current quest is listed in the morning "All quests" snapshot (see below).

All quests — live snapshot (morning ping only)

A separate Discord message listing every current MS Rewards perk/activity and every current Xbox quest with its progress, e.g.:

Bing — Search: 1/1
Daily Set — Activity: 3/3
Play a PC game -- 0/1 (20 pts, Available)
Weekly PC bonus -- 1/5 (260 pts, Expired)
  • MS Rewards' half is computed live, for free, during the same Kernel session as the Bing daily-set check above (it also visits /earn, not just the dashboard — no extra Kernel cost, just one more page navigation in the same browser session). Scoped to exactly three sections from Earn, per Connor's request (2026-09-12): Streaks, Explore on Bing, and Keep earning — not the dashboard's "Your perks", nor Earn's "Level up activities"/"Quests". Streaks are listed individually; Explore on Bing/Keep earning are summarized as counts (mostly one-off quiz/article cards, not meaningfully individual quests). See extractEarnPageLines in lib/checkers/msRewards.ts to adjust scope again later.
  • Xbox's half comes from a local PC script (C:\dev\xbox-quest-check, see below) that runs once each morning and reports in via quests.ts, since the Xbox app can only be read from Connor's own machine.

Why Xbox needs a local script — the short version

The Xbox app's quests have no web page and no public API (confirmed: checked xbox.com/Rewards, the signed-in xbox.com/play hub, site search, and actual network traffic — nothing). A full network-capture attempt with mitmproxy also came up empty even with loopback exemptions and both WinINET/WinHTTP proxy settings correctly configured — the app simply doesn't route through either proxy surface, not even a rejected connection attempt showed up.

What does work: the Xbox app's UI Automation tree (the same accessibility API screen readers use) already contains everything as plain text, e.g. "Weekly PC bonus, Play 5 days this week on PC for 15 minutes., 260 points, 1 out of 5 completed, Expired". C:\dev\xbox-quest-check\check.ps1 reads that tree directly — no cookies, no auth, no network capture, just reading what's already rendered on screen. Full details in that project's own README.

How the two Kernel-based checks actually work — read this before touching them

Both Brilliant and MS Rewards Bing use Kernel (real headless Chrome in the cloud) and both need their session cookie injected via Puppeteer's page.setCookie(), NOT page.setExtraHTTPHeaders({ Cookie: ... }).

This isn't a style preference — it was the difference between working and not. The debugging trail, for whenever this breaks again:

  1. A plain authenticated fetch() (no browser at all) genuinely can't see MS Rewards' data from Val Town's IP, Kernel's IP, or Connor's own home IP — always landed on the logged-out /about page.
  2. Tried: re-grabbing a fresher cookie, grabbing it via DevTools' Network tab instead of document.cookie (to catch httpOnly cookies), using "Copy value" instead of drag-select, adding a full realistic Chrome header set (Sec-Fetch-*, sec-ch-ua, etc.), forcing TLS 1.2. None of it worked — every one of those attempts still used a raw Cookie header override, whether via fetch() or Puppeteer's setExtraHTTPHeaders().
  3. Switching to page.setCookie() — letting Chrome's own cookie jar carry the session instead of forcing a header — worked immediately, first try, from Val Town's IP. Whatever Microsoft's edge was checking, it cared about how the cookie arrived, not just its value or freshness.

Lesson for future checkers on either of these sites (or similar Microsoft/Google-style properties): always use setCookie(), never setExtraHTTPHeaders for auth cookies.

Required setup

1. Discord webhook

✅ Already set.

2. Kernel API key

  1. Create a free account at https://onkernel.com.
  2. Go to Settings → API Keys → generate one.
  3. Paste it here:

👉 Add KERNEL_API_KEY here: https://www.val.town/x/CAS98/daily-streak-checklist/environment-variables?key=KERNEL_API_KEY

A real session credential — treat it like a password. It rides along in every request as proof you're logged in, and it expires periodically (you'll need to repeat this every so often when the check starts coming back "unverifiable").

  1. Log into https://rewards.bing.com in your browser as normal.
  2. Open DevTools (F12) → Network tab, then reload the page.
  3. Click the first request to rewards.bing.com, scroll to Request Headers, right-click the Cookie row → Copy value (not drag-select — long values wrap and drag-select can silently corrupt them).
  4. Paste it here:

👉 Add REWARDS_COOKIE here: https://www.val.town/x/CAS98/daily-streak-checklist/environment-variables?key=REWARDS_COOKIE

Same idea, same method (Network tab, not document.cookie — Brilliant's auth cookie is httpOnly):

  1. Log into https://brilliant.org, go to https://brilliant.org/you/.
  2. DevTools (F12) → Network tab → reload.
  3. Click the first request to brilliant.org → Request Headers → Copy value on Cookie.
  4. Paste it here:

👉 Add BRILLIANT_COOKIE here: https://www.val.town/x/CAS98/daily-streak-checklist/environment-variables?key=BRILLIANT_COOKIE

5. REPORT_SECRET — ⚠️ actively used, do not delete

Shared secret that quests.ts checks against, so only trusted local scripts (currently just the Xbox quest reader) can write into quest_snapshots. An earlier version of this README called it unused and safe to delete — that was wrong and caused a real outage (the Xbox script started getting 401s once it was deleted). It is used. If it ever needs regenerating, update it in both places — here and in C:\dev\xbox-quest-check's XBOX_REPORT_SECRET line in secrets.env — or every local reporter script breaks silently until someone notices missing quest data.

Known caveats (read before trusting the streaks blindly)

  • Both Kernel-based scrapers are unofficial — they parse undocumented page markup that can change without notice. If an item starts showing "⚠️ couldn't auto-verify" every run, check get_logs for this val — usually either a cookie expiring (re-grab via the steps above) or the site changing its markup, in which case the relevant file in lib/checkers/ needs a look.
  • Both cookies are live session tokens. Anyone who has one can act as your logged-in session on that site. They're stored encrypted as Val Town env vars (same protection as any other secret here), but they're inherently more sensitive than a typical API key — to kill one, sign out of that browser session and grab a fresh one.
  • Brilliant detection is "did the streak number change since last time we looked," not a true daily flag (Brilliant doesn't expose one). This is correct even across a missed-day reset, but if you complete two days' worth of lessons back-to-back before a scheduled check runs, only one day gets credited (the counter can't tell you did 2 vs 1).
  • Cron runs in UTC, fixed at 07:00 / 13:00 / 20:00 UTC — during UK daylight saving (BST, roughly late Mar–late Oct) that's 8am/2pm/9pm local; during GMT (winter) it drifts an hour earlier. Adjust the cron in morning.ts / afternoon.ts / evening.ts (via write_interval_settings) if that bugs you.
  • Streak day boundary is midnight Europe/London, independent of the ping times above.
  • Kernel costs a small amount per check (3x/day × 2 checkers = 6 headless browser sessions/day). Check Kernel's pricing if that matters.
  • The Xbox quest snapshot depends on a local Scheduled Task (XboxQuestCheck, ~7:40am daily) on Connor's PC, which itself depends on the Xbox app being launchable and the PC being on. If it's ever missing from the morning ping, check C:\dev\xbox-quest-check\check.log first.

Adding more habits later

Add an entry to ITEMS in lib/config.ts — a key, displayName, and a checker: () => Promise<CheckOutcome> — for something with a real daily done/not-done state. No other file needs to change — streak tracking and messaging pick it up automatically. But only add it if the checker can sometimes return { done: true } — an item whose checker always returns { done: null } (nothing to verify, ever) will just sit there permanently unfilled, exactly what happened with Xbox before it got pulled out. For that kind of thing (no clean daily boolean, or genuinely unverifiable), follow the quest-snapshot pattern below instead.

For a quest-list-style addition (rotating items, no clean daily done/not-done) rather than a streak item, follow the Xbox pattern instead: report into quest_snapshots via quests.ts (or compute inline if it can run in Val Town) and add it to buildQuestMessage in lib/runCheck.ts.

Files

  • morning.ts / afternoon.ts / evening.ts — the three cron entrypoints.
  • quests.ts — HTTP endpoint for external scripts (currently just the Xbox reader) to report a quest snapshot into quest_snapshots.
  • lib/runCheck.ts — orchestration: runs checkers, updates streaks, builds and sends the Discord messages (checklist + quest snapshot).
  • lib/checkers/ — one file per verified streak item.
  • lib/db.ts — SQLite-backed state: items/daily_checks (streaks) and quest_snapshots (live quest lists).
  • lib/discord.ts — webhook sender.
  • lib/config.ts — the list of tracked streak items + the CheckOutcome type.
  • C:\dev\xbox-quest-check (local, not in this val) — the Xbox UI Automation reader + its own Scheduled Task and README.

Testing

Use run_file on morning.ts (or afternoon.ts/evening.ts) to trigger a check manually instead of waiting for the schedule, then check get_logs for any checker warnings. To test the Xbox side, run C:\dev\xbox-quest-check\check.ps1 directly and check its check.log.