Expose the manipulation. Score the shame. Fix the web.
A full-stack tool that scans any website or email for deceptive UX dark patterns — hidden unsubscribe buttons, confirm-shaming, fake urgency, invisible opt-out links — and gives it an Sus Score from 0 to 100.
You've seen them. Websites that:
- Hide the "Unsubscribe" button in 7px grey text on a white background
- Say "No thanks, I enjoy losing money" when you try to cancel
- Pre-tick marketing checkboxes so you opt in without noticing
- Bury the cancel button 4 clicks deep while the "Subscribe" button is front and center
This tool catches all of it. Automatically.
| Feature | Description |
|---|---|
| Sus Score (0–100) | Animated gauge with per-category breakdown |
| 8 Sus Scores | Low contrast, tiny fonts, hidden elements, confirm-shaming, misleading labels, pre-ticked boxes, fake urgency, footer burial |
| Before/After Screenshots | Side-by-side original vs cleaned view via Puppeteer |
| Email Newsletter Scanner | Paste raw email HTML — first tool to detect dark patterns inside emails |
| Sus Score Timeline | Track how a site's dark patterns change week over week |
| "Fix It For Me" | Diff view showing the exact HTML changes needed |
| LinkedIn Post Generator | One-click ready-to-post report for sharing findings |
| Chrome Extension | Real-time Sus Score badge on every page you visit |
Try it live or run the demo mode locally (no database needed)
- Node.js 18+
- PostgreSQL 14+ (or skip DB for demo mode)
git clone https://github.com/findsri/sus-score.git
cd sus-score
npm install# Frontend only — uses built-in demo scan results
cd app
cp .env.example .env.local # NEXT_PUBLIC_DEMO_MODE=true
npm run dev
# → http://localhost:3000# 1. Set up env
cp api/.env.example api/.env
# Edit api/.env → set DATABASE_URL=postgresql://localhost/darkpatterns
echo "NEXT_PUBLIC_API_URL=http://localhost:4000" > app/.env.local
# 2. Init database + seed demo data
createdb darkpatterns
cd api && npx ts-node src/seed.ts && cd ..
# 3. Build shared detector package
cd packages/detector && npm install && npm run build && cd ../..
# 4. Start everything
npm run dev # starts both API (4000) and frontend (3000)- Go to
chrome://extensions - Enable Developer Mode (top right)
- Click Load Unpacked
- Select the
extension/folder
You'll see an Sus Score badge appear on every website you visit.
sus-score/
├── app/ # Next.js 14 + Tailwind frontend
│ ├── src/app/ # Pages (scanner, leaderboard, timeline, email)
│ └── src/components/ # SusScoreGauge, PatternCard, DiffViewer…
│
├── api/ # Express + TypeScript backend
│ ├── src/routes/scan.ts # POST /api/scan
│ ├── src/routes/leaderboard.ts
│ ├── src/db.ts # PostgreSQL schema
│ └── src/screenshot.ts # Puppeteer integration
│
├── packages/detector/ # Shared detection library (importable)
│ ├── src/detectors/ # unsubscribeDetector, generalDarkPatterns
│ ├── src/utils/scoring.ts # Sus Score algorithm
│ ├── src/linkedInGenerator.ts
│ └── src/fixGenerator.ts # HTML diff + auto-fix
│
└── extension/ # Chrome Manifest V3
├── background.js # Service worker + badge updates
├── content.js # Page scanner + injected badge
└── popup.html/js # Extension popup UI
{
"url": "https://example.com",
"html": "<html>…</html>",
"isEmail": false,
"takeScreenshot": false
}Response:
{
"scanId": "uuid",
"susScore": 74,
"totalPatterns": 9,
"scoreBreakdown": { "low_contrast": 18, "confirm_shaming": 16, … },
"patterns": [ { "category": "confirm_shaming", "severity": "high", … } ],
"linkedInPost": "🚫 I scanned linkedin.com and found 9 dark patterns…",
"diff": [ { "original": "…", "fixed": "…", "description": "…" } ]
}| Endpoint | Description |
|---|---|
POST /api/scan |
Scan a URL or HTML |
GET /api/scan/:id |
Retrieve a past scan |
GET /api/leaderboard |
Hall of Shame top 50 |
GET /api/leaderboard/timeline/:domain |
Score history for a domain |
GET /api/leaderboard/recent |
Recent public scans |
Each detected pattern adds weighted points to a category bucket (capped per category). The total is normalized to 0–100.
| Category | Max Points | What triggers it |
|---|---|---|
| Low Contrast | 25 | Contrast ratio < 4.5:1 on opt-out elements |
| Hidden Element | 20 | display:none / visibility:hidden on unsubscribe |
| Confirm Shaming | 20 | "No thanks, I enjoy losing money" language |
| Opacity Hidden | 20 | Opacity < 0.3 on opt-out elements |
| Off-Screen | 20 | Absolute positioned far off-screen |
| Misleading Label | 15 | Pre-ticked checkboxes, vague "click here" labels |
| Tiny Font | 15 | Font size < 10px on opt-out text |
| Buried in Footer | 10 | Unsubscribe only exists in <footer> |
| No Link Styling | 10 | text-decoration:none making links invisible |
- Frontend: Next.js 14 (App Router) · Tailwind CSS · Framer Motion · Recharts
- Backend: Express · TypeScript · Zod · express-rate-limit
- Detection: Cheerio · wcag-contrast
- Screenshots: Puppeteer
- Database: PostgreSQL
- Extension: Chrome Manifest V3 (vanilla JS, no build step)
Frontend → Vercel
cd app && vercel deployBackend + DB → Railway
- Connect this repo to Railway
- Add a PostgreSQL plugin
- Set
DATABASE_URLfrom the plugin - Set root to
api/→ deploy
Found a dark pattern this tool misses? Open an issue or PR. Ideas for new detection rules are especially welcome.
MIT
If this helped you, star the repo ⭐ and share it.