Inspiration

Every platform death spiral starts the same way: moderators burn out, communities decay, users migrate. Digg collapsed when its power users left. Tumblr's governance failures drove creators to Instagram. Reddit's moderators collectively perform billions of dollars in unpaid labor annually — and Reddit has no early warning system for when that foundation cracks. We built DecayWatch because we watched mod teams silently disintegrate under uneven workloads, and realized the platform's survival depends on predicting that collapse before it happens.


What It Does

DecayWatch is an algorithmic health monitor for subreddit moderation teams. It captures every mod action in real-time via Devvit triggers, computes a composite 0-100 Health Score from four weighted signals — queue velocity (25%), response time (25%), workload distribution fairness via Gini coefficient (30%), and rule effectiveness (20%) — and posts automated YELLOW/RED alert posts to the mod team when burnout thresholds breach. A pinned custom post dashboard auto-refreshes every 15 minutes showing the score, 7-day trend sparkline, per-mod activity bars, and top offending rules. All data persists in Reddit's native Redis with automatic pruning. Zero external servers. Zero API keys. Pure Devvit.

System overview

┌─────────────────────────────────────────────────────────────────┐
│                        REDDIT PLATFORM                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐   │
│  │   Posts     │  │  Comments   │  │    Mod Actions      │   │
│  │  (create)   │  │  (create)   │  │ (approve/remove/...)│   │
│  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘   │
│         └────────────────┴────────────────────┘               │
│                          │                                     │
│                    ┌─────┴─────┐                               │
│                    │  DEVVIT   │                               │
│                    │  Runtime  │                               │
│                    └─────┬─────┘                               │
└──────────────────────────┼────────────────────────────────────┘
                           │
┌──────────────────────────┼────────────────────────────────────┐
│                    DECAYWATCH APP                              │
│                                                                │
│  ┌───────────────────────┐    ┌──────────────────────────┐   │
│  │    TRIGGER LAYER      │    │      JOB LAYER           │   │
│  │  (Real-time Capture)  │    │  (Scheduled Computation) │   │
│  │                       │    │                          │   │
│  │  ┌─────────────────┐  │    │  ┌──────────────────┐   │   │
│  │  │ onModAction.ts  │  │    │  │ computeHealth.ts │   │   │
│  │  │ • Mod action →  │  │    │  │ • Hourly: runs   │   │   │
│  │  │   Redis sorted  │  │    │  │   all 4 scorers  │   │   │
│  │  │   set (ts score)│  │    │  │ • Writes snapshot│   │   │
│  │  └─────────────────┘  │    │  │   to Redis       │   │   │
│  │                       │    │  └──────────────────┘   │   │
│  │  ┌─────────────────┐  │    │                          │   │
│  │  │ onPostSubmit.ts │  │    │  ┌──────────────────┐   │   │
│  │  │ • Post creation │  │    │  │burnoutDetection  │   │   │
│  │  │   time → Redis  │  │    │  │ • Daily 9am UTC  │   │   │
│  │  │   hash + sorted │  │    │  │ • Checks thresh. │   │   │
│  │  │   set           │  │    │  │ • Posts YELLOW/  │   │   │
│  │  └─────────────────┘  │    │  │   RED alert      │   │   │
│  │                       │    │  └──────────────────┘   │   │
│  │  ┌─────────────────┐  │    │                          │   │
│  │  │  onInstall.ts   │  │    │  ┌──────────────────┐   │   │
│  │  │ • Backfill 30d  │  │    │  │   pruneData.ts   │   │   │
│  │  │   mod log       │  │    │  │ • Weekly: deletes│   │   │
│  │  │ • Create welcome│  │    │  │   expired keys   │   │   │
│  │  │   post          │  │    │  │   (90d/30d/60d)  │   │   │
│  │  └─────────────────┘  │    │  └──────────────────┘   │   │
│  └───────────────────────┘    └──────────────────────────┘   │
│                                                                │
│  ┌───────────────────────┐    ┌──────────────────────────┐   │
│  │    SCORING LAYER      │    │      UI LAYER            │   │
│  │  (The Math)           │    │  (Dashboard & Alerts)    │   │
│  │                       │    │                          │   │
│  │  ┌─────────────────┐  │    │  ┌──────────────────┐   │   │
│  │  │ healthScore.ts  │  │    │  │  dashboard.tsx   │   │   │
│  │  │ • Aggregates 4  │  │    │  │ • Custom Post    │   │   │
│  │  │   scorers 0–100 │  │    │  │   (Devvit Blocks)│   │   │
│  │  │ • V25% R25%     │  │    │  │ • Auto-refresh   │   │   │
│  │  │   D30% Ru20%    │  │    │  │   every 15 min   │   │   │
│  │  └─────────────────┘  │    │  └──────────────────┘   │   │
│  │                       │    │                          │   │
│  │  ┌─────────────────┐  │    │  ┌──────────────────┐   │   │
│  │  │ queueVelocity   │  │    │  │  charts.html     │   │   │
│  │  │ • EMA baseline  │  │    │  │ • WebView escape │   │   │
│  │  │ • Spike ratio   │  │    │  │   hatch for      │   │   │
│  │  │   vs trend      │  │    │  │   Chart.js viz   │   │   │
│  │  └─────────────────┘  │    │  └──────────────────┘   │   │
│  │                       │    │                          │   │
│  │  ┌─────────────────┐  │    │  ┌──────────────────┐   │   │
│  │  │ responseTime    │  │    │  │  Alert Posts     │   │   │
│  │  │ • 7-day window  │  │    │  │ • Auto-generated │   │   │
│  │  │ • ≤30min = 100  │  │    │  │   YELLOW/RED to  │   │   │
│  │  │   ≥4hrs  = 0    │  │    │  │   subreddit      │   │   │
│  │  └─────────────────┘  │    │  │ • 24h dedup TTL  │   │   │
│  │                       │    │  └──────────────────┘   │   │
│  │  ┌─────────────────┐  │    └──────────────────────────┘   │
│  │  │ burnoutRisk     │  │                                    │
│  │  │ • Gini coeff on │  │    ┌──────────────────────────┐   │
│  │  │   mod action    │  │    │      STORAGE LAYER       │   │
│  │  │   distribution  │  │    │      (Redis Schema)      │   │
│  │  │ • 0.1=100 0.8=0 │  │    │                          │   │
│  │  └─────────────────┘  │    │  ┌──────────────────┐   │   │
│  │                       │    │  │     keys.ts      │   │   │
│  │  ┌─────────────────┐  │    │  │ • Central schema │   │   │
│  │  │ ruleEffective.  │  │    │  │   prefix:        │   │   │
│  │  │ • Repeat offend │  │    │  │   dw:{sub}:...   │   │   │
│  │  │   rate per rule │  │    │  └──────────────────┘   │   │
│  │  │ • 30-day window │  │    │                          │   │
│  │  └─────────────────┘  │    │  ┌──────────────────┐   │   │
│  └───────────────────────┘    │  │  modActions.ts   │   │   │
│                               │  │ • Sorted set     │   │   │
│  ┌───────────────────────┐    │  │ • TTL: 90 days   │   │   │
│  │    UTILITIES          │    │  └──────────────────┘   │   │
│  │  ┌─────────────────┐  │    │                          │   │
│  │  │  constants.ts   │  │    │  ┌──────────────────┐   │   │
│  │  │ • Score weights │  │◄───┤  │ postTimestamps   │   │   │
│  │  │ • Thresholds    │  │    │  │ • Hash + sorted  │   │   │
│  │  │ • Redis TTLs    │  │    │  │ • TTL: 30 days   │   │   │
│  │  └─────────────────┘  │    │  └──────────────────┘   │   │
│  │  ┌─────────────────┐  │    │                          │   │
│  │  │    math.ts      │  │    │  ┌──────────────────┐   │   │
│  │  │ • Gini calc     │  │    │  │    scores.ts     │   │   │
│  │  │ • EMA / rolling │  │    │  │ • Sorted set     │   │   │
│  │  └─────────────────┘  │    │  │   (history)      │   │   │
│  │  ┌─────────────────┐  │    │  │ • String (latest)│   │   │
│  │  │    time.ts      │  │    │  │ • TTL: 60 days   │   │   │
│  │  │ • UTC helpers   │  │    │  └──────────────────┘   │   │
│  │  │ • Window bounds │  │    │                          │   │
│  │  └─────────────────┘  │    │  ┌──────────────────┐   │   │
│  └───────────────────────┘    │  │    alerts.ts     │   │   │
│                               │  │ • String dedup   │   │   │
│                               │  │ • TTL: 24 hours  │   │   │
│                               │  └──────────────────┘   │   │
│                               └──────────────────────────┘   │
└────────────────────────────────────────────────────────────────┘

Data flow

[Mod approves post on Reddit]
         │
         ▼
┌─────────────────┐
│  onModAction    │
│  trigger fires  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│  Redis: write   │     │  Redis: write   │
│  mod action to  │     │  post timestamp │
│  sorted set     │     │  (if new post)  │
│  (TTL: 90d)     │     │  (TTL: 30d)     │
└─────────────────┘     └─────────────────┘
         │
         ▼
[Every hour: computeHealth job runs]
         │
         ▼
┌─────────────────────────────────────────┐
│  1. queueVelocity.ts → EMA vs baseline  │
│  2. responseTime.ts → 7-day avg delta   │
│  3. burnoutRisk.ts  → Gini on mod dist  │
│  4. ruleEffectiveness.ts → repeat rate  │
│                                         │
│  healthScore.ts aggregates:             │
│  0.25×V + 0.25×R + 0.30×D + 0.20×Ru   │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│  Write snapshot to Redis:               │
│  • health_scores sorted set (60d TTL)   │
│  • latest_score string (fast read)      │
└─────────────────┬───────────────────────┘
                  │
                  ▼
[Dashboard reads latest_score every 15 min]
                  │
                  ▼
[Daily 9am UTC: burnoutDetection runs]
                  │
                  ▼
┌─────────────────────────────────────────┐
│  Check: Gini > 0.6?                     │
│         Response > 4 hrs?               │
│         Velocity spike > 200%?          │
│                                         │
│  If YES → Check alert_dedup:* (24h TTL) │
│           If no dedup → Post YELLOW/RED │
│           alert to subreddit            │
└─────────────────────────────────────────┘

Data lifecycle

Data type Retention Prune mechanism
Mod actions 90 days Sorted set zRemRangeByScore
Post timestamps 30 days Hash + sorted set index
Health scores 60 days Sorted set zRemRangeByScore
Alert dedup 24 hours Redis key TTL (auto-expires)

How We Built It

Architecture: 29-file TypeScript codebase on Devvit with strict separation of concerns — triggers for real-time ingestion, scheduled jobs for computation, scoring layer for math, storage layer for Redis schemas, and UI layer for the dashboard.

Data Pipeline: onModAction and onPostSubmit triggers write to Redis sorted sets with millisecond timestamps. computeHealth runs hourly (every minute in playtest) to execute four scorers: queue velocity uses exponential moving averages against baseline, response time calculates post-to-action deltas in a 7-day window, burnout risk applies Gini coefficient to per-mod action distribution (0.1 = perfect equality, 0.8 = one mod does everything), and rule effectiveness tracks repeat-offender rates per removal rule.

Alert System: burnoutDetection runs daily at 9am UTC, checks if Gini > 0.6 OR response time > 4hrs OR velocity spike > 200%, and posts a deduplicated alert (24h TTL) to the subreddit with specific recommendations.

UI: Custom Devvit Blocks post with useState for async data loading, useInterval for 15-minute refresh, and a Chart.js WebView escape hatch for richer visualizations.


Directory structure

decaywatch/
├── src/
│   ├── main.ts                       # Single entry — registers everything
│   ├── triggers/
│   │   ├── onModAction.ts            # Captures every mod action → Redis
│   │   ├── onPostSubmit.ts           # Records post submission times
│   │   └── onInstall.ts              # 30-day backfill + welcome post
│   ├── jobs/
│   │   ├── computeHealth.ts          # Hourly score computation
│   │   ├── burnoutDetection.ts       # Daily threshold check + alerting
│   │   └── pruneData.ts              # Weekly Redis cleanup
│   ├── scoring/
│   │   ├── healthScore.ts            # Weighted aggregate (0–100)
│   │   ├── burnoutRisk.ts            # Gini coefficient on mod distribution
│   │   ├── queueVelocity.ts          # Items/hour + spike vs. baseline
│   │   ├── responseTime.ts           # Post-to-action delta
│   │   └── ruleEffectiveness.ts      # Repeat-offender rate per rule
│   ├── storage/
│   │   ├── keys.ts                   # All Redis key schemas
│   │   ├── modActions.ts             # Sorted-set read/write for mod actions
│   │   ├── postTimestamps.ts         # Hash + index for post times
│   │   ├── scores.ts                 # Health score snapshots
│   │   └── alerts.ts                 # Alert dedup keys
│   ├── reddit/
│   │   ├── modlog.ts                 # getModerationLog wrapper + pagination
│   │   └── queue.ts                  # getModQueue wrapper
│   ├── ui/
│   │   ├── dashboard.tsx             # Custom Post render function
│   │   ├── components/
│   │   │   ├── BurnoutBadge.tsx      # RED / YELLOW / GREEN badge
│   │   │   ├── HealthGauge.tsx       # Large score display
│   │   │   ├── ModActivityBar.tsx    # Per-mod horizontal bar chart
│   │   │   ├── RuleTable.tsx         # Top rules + repeat rates
│   │   │   └── TrendLine.tsx         # 7-day sparkline (block bars)
│   │   └── webview/
│   │       └── charts.html           # Chart.js full visualisation
│   └── utils/
│       ├── constants.ts              # All magic numbers in one place
│       ├── time.ts                   # UTC helpers
│       └── math.ts                   # Gini, rolling avg, normalisation
├── devvit.yaml                       # Manifest, permissions, scheduler crons
├── package.json
├── tsconfig.json
└── README.md

Challenges We Ran Into

Devvit Platform Quirks: Custom post types don't register on app updates — only on fresh installs. We burned two hours debugging why Create Post → Apps was empty before discovering the version-bump workaround (0.0.1 → 0.0.2 → clean reinstall).

Redis Schema Rigidity: Devvit's Redis implementation lacks TTL granularity on sorted set members, forcing us to implement manual pruning via pruneData weekly jobs instead of native expiration.

Gini Coefficient on Sparse Data: Early installs have <7 days of history, making Gini calculations statistically noisy. We added minimum-action thresholds (30 actions across team) before the scorer activates, preventing false RED alerts on new communities.

Real-time vs. Scheduled Tension: Triggers fire instantly but scoring needs batch computation. We optimized by storing raw events in Redis and computing aggregates in jobs, rather than calculating on every trigger (which would hit Devvit execution limits).


Accomplishments We're Proud Of

Gini Coefficient in Production: Most hackathon projects use simple averages. We implemented actual economic inequality measurement (the same metric the World Bank uses) to quantify moderator workload fairness. A Gini of 0.72 means one moderator is doing 90% of the work — a number no human intuition would surface.

Zero-to-One Data Pipeline: From trigger ingestion to computed score to alert post to dashboard render — every layer talks to every other layer correctly. The end-to-end flow works without external dependencies.

Deduplicated Alert System: Burnout detection without spam prevention is useless. Our 24h TTL deduplication means a mod team gets warned, not harassed.

Auto-refreshing Dashboard: The custom post re-renders every 15 minutes with fresh Redis data. No manual refresh. No page reload. It just lives.


What We Learned

Devvit Is More Powerful Than Documented: The platform supports complex multi-file architectures, scheduled cron jobs, and Redis persistence — but the docs bury these capabilities. We learned by reading source code and experimenting.

Moderation Is an Infrastructure Problem, Not a Content Problem: Reddit thinks its product is posts and comments. After building DecayWatch, we're convinced the product is governance-at-scale. The math proves it.

Burnout Has Predictable Signatures: Before this project, we assumed mod burnout was emotional/psychological. The data shows it's mathematical — workload inequality + queue velocity spikes + response time decay = resignation within 48-72 hours.


What's Next for modnecro

Phase 1 — Cross-Subreddit Analytics: Aggregate anonymized health scores across installs to show comparative benchmarks ("Your sub is healthier than 73% of similar communities").

Phase 2 — Predictive Scheduling: Auto-generate mod duty rotations based on historical activity patterns ("Assign u/Alice to weekends, u/Bob to nights").

Phase 3 — Governance Export: Portable subreddit rules, automations, and reputation systems that work on Discord, forums, and decentralized protocols. Reddit doesn't own the communities. It owns the governance standard they depend on.

Phase 4 — Moderator Labor Marketplace: Infrastructure for communities to compensate their own mods, with Reddit taking a transaction cut. Top mods become governance professionals with reputation scores and cross-community consulting value.

Built With

Share this project:

Updates