Score daily sign-in events with streak multiplier
Opened by stack72 · 4/9/2026· GitHub #230
Summary
Track daily sign-ins as scoring events with an escalating streak multiplier — consecutive daily logins earn progressively more points.
Motivation
Rewarding daily engagement encourages operatives to stay active in the swamp. A streak multiplier makes consistent daily presence increasingly valuable, creating a compelling reason to log in regularly. This is a retention mechanic that also feeds into leaderboard competition.
Design sketch
- Event source: Auth middleware or session creation emits a daily sign-in event (at most once per calendar day per user)
- Event shape:
{ event: "daily_sign_in", distinct_id: "...", properties: { username, date } } - Streak tracking: Track consecutive sign-in days per user. Each day signed in increments the streak; a missed day resets it.
- Scoring: Base points × streak multiplier. The multiplier grows with consecutive days (e.g., day 1 = 1×, day 2 = 1.5×, day 7 = 3×, day 30 = 5×). Exact curve TBD.
- Deduplication: One
daily_sign_inper username per calendar day, idempotent on reprocessing - Storage: Streak state (current streak length, last sign-in date) stored per user — either in
username_metricsor a dedicated collection
Open questions
- Multiplier curve — linear, logarithmic, tiered thresholds?
- Should there be a multiplier cap to prevent runaway scores?
- Grace period for missed days (e.g., 1 day grace before streak resets)?
- Should streak length be visible on the user's profile?
- Relationship to
activeDatesalready tracked in telemetry metrics — can we derive streaks from existing data? - Relationship to #227 (award events) and #229 (sign-up scoring)
Open
No activity in this phase yet.
stack72 commented 4/9/2026, 4:43:31 PM
Originally posted by @johnrwatson on 2026-03-12T23:34:22Z:
Implementation Plan: Score daily sign-in events with streak multiplier
Context
The app tracks CLI activity via telemetry and scores it in UserScore. There's no tracking of web sign-ins as a scoring signal. This feature adds a daily sign-in contribution to UserScore with a streak multiplier that rewards consecutive daily visits, creating a retention mechanic.
Key distinction: the existing computeStreak() in activity-score.ts operates on CLI telemetry activeDates. This feature tracks web presence separately via a new sign_in_dates field in username_metrics, flowing through the existing telemetry pipeline.
Design Decisions
- Ingest via telemetry pipeline — middleware emits
daily_sign_inevents throughtrack(), flowing through the same telemetry service → StatsConsumer → event queue → processScoreBatch pipeline as CLI events. - New
sign_in_datesfield inusername_metrics— StatsConsumer tracks sign-in dates separately fromactive_dates(which tracks all event types). This gives the scoring layer a clean signal for web-only engagement. - Middleware trigger with in-memory dedup — "daily sign-in" means first authenticated visit per calendar day. In-memory cache (username → date) prevents redundant
track()calls;$addToSetat the DB level provides idempotency as a safety net. - No grace period — miss a day, lose the streak. Keeps the daily login incentive sharp.
- No aggregate class — per DDD Design Gate, pure functions + record type. Streak is computed from the
signInDatesarray (same pattern ascomputeStreak()fromactiveDates). - Contribution key
"community:daily-sign-in"— overwrites each time (not cumulative), so points reflect current streak level. "engagement"category — distinct from CLI"activity"category.- Profile visibility deferred — streak display on user profiles will be a follow-up issue.
Multiplier Curve (tiered)
| Streak Days | Multiplier | Points (10 base) |
|---|---|---|
| 1 | 1× | 10 |
| 2–6 | 1.5× | 15 |
| 7–13 | 2× | 20 |
| 14–29 | 3× | 30 |
| 30+ | 5× (cap) | 50 |
Data Flow
Middleware (track "daily_sign_in")
→ Telemetry service /ingest
→ StatsConsumer: $addToSet sign_in_dates on username_metrics
→ Queue producer: snapshot includes signInDates
→ Main app event queue watcher
→ processScoreBatch → refreshSignInScore
→ UserScore.contribute("community:daily-sign-in", ...)Files to Create
1. lib/domain/scoring/sign-in-score.ts — Domain (pure functions)
computeSignInStreak(signInDates: string[], now: Date): number— consecutive sign-in days ending today (no grace period, unlikecomputeStreakinactivity-score.ts)computeSignInPoints(streak: number): number— tiered multiplier curve above
2. lib/app/refresh-sign-in-score.ts — App service
Follows the exact pattern of refresh-activity-score.ts:
- Computes streak via
computeSignInStreak(signInDates, new Date()) - Computes points via
computeSignInPoints(streak) - Updates UserScore with
withRetrypattern:score.contribute("community:daily-sign-in", "community", "engagement", points)
3. tests/domain/sign-in-score_test.ts — Domain unit tests
computeSignInStreak: no dates (0), single date today (1), consecutive days (correct count), gap resets (1), dates not including today (0 — no grace period)computeSignInPoints: boundary values at each tier (1, 2, 6, 7, 13, 14, 29, 30, 100)
Files to Modify
4. services/telemetry/lib/consumers/stats.ts — StatsConsumer + UserMetrics type
- Add
sign_in_dates?: string[]to theUserMetricsinterface - In the per-username metrics update block, add conditional
sign_in_datestracking fordaily_sign_inevents via$addToSet
5. shared/queue-schema.ts — Queue contract
Add signInDates?: string[] to the metrics snapshot type (optional for backward compat).
6. services/telemetry/lib/queue-producer.ts — Snapshot builder
Include sign_in_dates from username_metrics doc in the snapshot.
7. lib/app/refresh-user-score.ts — Score refresh orchestrator
Add call to refreshSignInScore() alongside existing activity and extension score refreshes.
8. lib/app/process-score-batch.ts — Batch processor
Pass signInDates from snapshot through to score refresh.
9. routes/_middleware.ts — Event emission
Add after session resolution, before verification gate:
- Module-level in-memory cache:
Map<string, string>(username → last recorded UTC date, max 10k entries) - Fire-and-forget
track("daily_sign_in", userId, { username })— same pattern as existingtrack("page_view", ...)
🤖 Generated with Claude Code
Sign in to post a ripple.