Skip to content

Trust-weighted crypto forum. Your reach is earned through good judgment, not bought. Invite-only, dark-mode, Twitter-style UI. Full-stack TypeScript: React + Hono + PostgreSQL.

Notifications You must be signed in to change notification settings

simondice/crypto-trust-feed

Repository files navigation

CryptoCunts

A trust-weighted crypto forum where your reputation is your currency.

No algorithms deciding what you see. No pay-to-play promoted posts. No blue checkmarks for sale. Just a number: your trust score. Earn it by making good calls. Lose it by being wrong, lazy, or a shill. Every vote you cast is a bet on your own judgment — and the system keeps score.

Think of it as a social credit system, but for crypto opinions. Except unlike the dystopian ones, this one is transparent, and you opted in.


The Big Idea

Every user gets a trust score from 0 to 1000. You start at 100 (a nobody). Your score goes up when the community validates your judgment. It goes down when you're consistently wrong, or when you invite someone who turns out to be garbage.

Your trust score affects everything:

  • Your vote weight — A vote from someone at 900 trust moves the needle 9x more than a vote from someone at 100. Your opinions literally carry more weight as you prove yourself.
  • Your feed position — Posts are ranked by a formula that combines trust-weighted votes, time decay, and author reputation. High-trust authors with upvotes from high-trust users dominate the feed. Spam sinks.
  • Your visibility — Authors with trust below 50 are hidden from the feed entirely. They still exist, but nobody has to see them unless they go looking.
  • Your inviter's reputation — You were invited by someone. If your trust tanks below 50, they take a hit. So people think twice before handing out invite codes to randoms.

The forum is invite-only. No invite code, no account. Every user is accountable to the person who let them in.


How Trust Actually Works (The Math)

Vote Settlement (Deferred)

Votes don't affect trust instantly. They marinate for 24 hours first. This prevents gaming via rapid vote/unvote cycles — if you remove your vote before settlement, it's like it never happened.

After 24 hours, the settlement cron processes surviving votes:

delta = vote_direction * (voter_trust / 1000) * 2.0 * diminishing_factor

The diminishing factor prevents one person from farming another's score. If you've already voted on 3 of someone's posts, your 4th vote only has 25% impact: factor = 1 / (1 + prior_votes_on_same_author).

Calibration (Every 8 Hours)

A cron job looks at all votes from the past 24-72 hours (randomized window) and asks: did you vote with or against the consensus?

Consensus uses a blended formula — 60% trust-weighted average + 40% raw average. This prevents a small clique of high-trust users from defining "correct" by themselves.

weighted_consensus = sum(voter_trust * vote) / sum(voter_trust)
raw_consensus      = sum(vote) / count(votes)
consensus          = 0.6 * weighted + 0.4 * raw

Then:

  • Controversial content (consensus between -0.2 and 0.2): skipped entirely. No punishment for voting on genuinely divisive stuff.
  • Aligned with consensus: you gain trust. Early voters get a bigger reward — reward = 0.5 / sqrt(vote_position). First voter gets the full 0.5, fourth voter gets 0.25, ninth gets 0.17. This incentivizes having the balls to vote early rather than piling on.
  • Against consensus: flat -1.0 penalty. No discount for being late to the wrong take.

Trust Decay (Daily)

All trust scores drift toward a baseline of 200 at a rate of 0.5% of the distance per day. This means:

  • A whale at 900 loses ~3.5 trust/day (if they stop participating)
  • A newbie at 50 gains ~0.75 trust/day (slow natural recovery)
  • Someone at exactly 200 is in equilibrium

This prevents permanent trust aristocracies and gives penalized users a recovery path. You can't coast on past glory — stay active or fade.

Anti-Sybil: Vote Cap

50 votes per day, rolling 24-hour window. You can't create 10 accounts and upvote yourself from each one at scale. Combined with diminishing returns, coordinated manipulation gets expensive fast.

Probation

New users get 3 boosted posts with enhanced visibility. If 60%+ of votes on any boosted post are dislikes, your trust gets slammed to 50 (not zero — we're harsh but not cruel). From there, daily decay slowly pulls you back toward 200 if you shut up and lurk.

Invite Shame

You invited someone. They turned out to be a spammer. Their trust drops below 50? You lose 5.0 trust. Once per invitee. Think of it as co-signing a loan — you're on the hook if they default.

Feed Ranking

Posts aren't sorted chronologically. They're sorted by:

score = trust_weighted_vote_sum / (hours_since_post + 2)^1.5 * (1 + author_trust / 1000)

Breaking that down:

  • trust_weighted_vote_sum: sum(voter_trust * vote_direction) — upvotes from trusted users count more
  • time decay: (hours + 2)^1.5 — posts lose steam over time. The +2 prevents division by zero and gives new posts a grace period
  • author multiplier: 1 + trust/1000 — a post by someone at 800 trust gets 1.8x the ranking boost of someone at 0

Authors below trust 50 are hidden from the feed. Their posts still exist at their direct URL, but they don't pollute the timeline.


Tech Stack

This is a full-stack TypeScript application. No Python, no Java, no microservices — just two Node processes and a Postgres database.

Frontend

Tech Why
React 18 You know what it is
TypeScript Strict mode off because life's too short, but types everywhere that matters
Vite Build tool. Dev server on port 8080 with API proxy to backend
Tailwind CSS Utility classes. Dark theme only — Bloomberg terminal aesthetic, no light mode
shadcn/ui 40+ pre-built components. Buttons, inputs, modals, toasts — the boring stuff handled
React Query (TanStack) Every API call is a hook. Optimistic vote updates, auto cache invalidation
React Router 6 Routes: /, /login, /signup, /user/:username, /admin
Sonner Toast notifications that actually look good
Lucide Icons. 1000+ of them, tree-shakeable
date-fns "3 hours ago" timestamps

Backend

Tech Why
Hono Ultra-lightweight API framework. Express but 10x faster and actually typed
Drizzle ORM Type-safe SQL. Schema-as-code, migrations, the whole deal
PostgreSQL The database. 8 tables, proper indexes, foreign keys, constraints
bcrypt Password hashing, 12 rounds. Timing-safe login (dummy hash on user-not-found)
JWT (HS256) Auth tokens. 24h expiry, issuer/audience validation
node-cron Scheduled jobs: calibration every 8h, trust decay daily at 03:00
Zod Request validation on every endpoint. UUID params, string lengths, enums

Design

  • Always dark. CSS variables for everything. No theme toggle.
  • Fonts: Inter for body, JetBrains Mono for trust scores and numbers
  • Colors: Trust uses a traffic light system (green >= 700, yellow >= 400, red < 400). Each category has its own color: Bitcoin orange, Ethereum purple, Altcoins teal, Politics red, Shitpost magenta.
  • Layout: Twitter-style. Avatar on the left, content on the right. Posts separated by dividers, not cards. Frosted glass blur on sticky headers.
  • Mobile: Sidebars collapse into slide-over overlays. Bottom nav bar with Home, Categories, New Post (+), More.

Database Schema

8 tables, all with proper UUIDs, timestamps, foreign keys, and indexes:

Table Purpose Rows you should know about
users Accounts trust_score (real, 0-1000), probation_posts_remaining (null = full member), invited_by (FK to users)
categories Forum sections 5 parents + 8 children. Parent/child via parent_id self-reference
posts Content is_boosted for probation posts. image_url optional
comments Threaded replies parent_comment_id for nesting. Max depth 5, max 500 per post
votes Like/dislike Unique constraint on (user_id, target_type, target_id). One vote per user per thing
invites Invite codes code (base64url, 8 random bytes). used_by tracks who claimed it
trust_events Audit log Every trust change: vote_settled, calibration, trust_decay, invite_penalty, probation_reset
reports Content flags reason: spam, impersonation, illegal, repost. status: pending/upheld/rejected

API Endpoints

Auth

Method Path Auth What it does
POST /auth/signup No Create account. Requires invite code. Username 3-30 chars, alphanumeric + underscore. Password needs upper, lower, number, 8+ chars
POST /auth/login No Returns JWT. Rate limited: 10 attempts per 15 min
GET /auth/me Yes Your profile, trust score, invites remaining, probation status

Posts

Method Path Auth What it does
GET /posts No Trust-ranked feed. ?category=, ?page=, ?limit=. Parent categories include children
GET /posts/:id No Single post with vote/comment counts
POST /posts Yes Create post. Auto-handles probation boost
PUT /posts/:id Yes Edit own post within 15-minute window
DELETE /posts/:id Yes Delete own post

Comments

Method Path Auth What it does
GET /posts/:id/comments No Threaded comment tree (assembled server-side)
POST /posts/:id/comments Yes Reply. Optional parentCommentId for threading
DELETE /comments/:id Yes Delete own comment

Votes

Method Path Auth What it does
POST /votes Yes Cast vote. Same vote again = toggle off. 50/day cap
DELETE /votes/:targetType/:targetId Yes Remove vote

Other

Method Path Auth What it does
GET /categories No All 13 categories with live post counts
POST /invites Yes Generate invite code (5/hour limit)
GET /invites Yes Your invites + invitee trust scores
POST /reports Yes Flag content. No self-reporting, no duplicates
GET /users/:username No Public profile + 50 most recent posts
GET /search?q= No ILIKE search on post content (min 2 chars)
POST /uploads Yes Image upload: JPEG/PNG/GIF/WebP, 5MB, magic byte validated

Admin (isAdmin: true required)

Method Path What it does
GET /admin/users List all users with email
PUT /admin/users/:id Edit trust score, admin status, invites
DELETE /admin/posts/:id God-mode delete any post
DELETE /admin/comments/:id God-mode delete any comment
POST /admin/invites Bulk generate invite codes (1-50)
GET /admin/invites All invites with usage status
GET /admin/stats Total users, posts, comments

Security

Not a toy. Actual security measures:

  • JWT: HS256, 24h expiry, issuer + audience validation. No refresh tokens (yet).
  • Passwords: bcrypt, 12 rounds. Timing-safe login — if the user doesn't exist, we still run a dummy bcrypt compare so the response time doesn't leak whether the account exists.
  • Rate limiting: Sliding window, in-memory. Auth (5 signup/15min, 10 login/15min), votes (60/min), uploads (10/min), invites (5/hour), reports (10/hour).
  • Input validation: Zod schemas on every endpoint. UUID validation on all route params. String length limits everywhere.
  • SQL injection: Drizzle ORM parameterizes everything. ILIKE special characters (%, _, \) are escaped.
  • Image uploads: Content-Type whitelist + magic byte verification (checks actual file header bytes, not just the extension). 5MB limit.
  • Headers: CSP (default-src 'self'), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin.
  • Body limit: 1MB on all endpoints.
  • Atomic operations: Invite claiming and probation decrement use SQL atomic operations to prevent race conditions.
  • No data leaks: Emails and internal IDs never appear in public API responses.

Quick Start

Prerequisites

  • Node.js 18+
  • PostgreSQL 14+

1. Clone and install

git clone https://github.com/simondice/crypto-trust-feed.git
cd crypto-trust-feed
npm install
cd server && npm install && cd ..

2. Set up PostgreSQL

CREATE USER cryptocunts WITH PASSWORD 'your-password';
CREATE DATABASE cryptocunts OWNER cryptocunts;

3. Configure environment

# server/.env
DATABASE_URL=postgresql://cryptocunts:your-password@localhost:5432/cryptocunts
JWT_SECRET=your-secret-at-least-32-characters-long
PORT=3000

4. Run migrations and seed

cd server
npx drizzle-kit migrate    # Create all 8 tables
npx tsx src/db/seed.ts      # Seed 13 categories

5. Create your first invite code

No invite code exists yet, so you need to bootstrap one:

psql -U cryptocunts -d cryptocunts -c "
  INSERT INTO invites (inviter_id, code)
  VALUES ('00000000-0000-0000-0000-000000000001', 'BOOTSTRAP');
"

Sign up with code BOOTSTRAP. After that, generate invites from the admin panel.

6. Start dev servers

# Terminal 1 — backend (port 3000)
cd server && npm run dev

# Terminal 2 — frontend (port 8080, proxies API to backend)
npm run dev

Open http://localhost:8080. Sign up, make yourself admin via SQL if needed:

UPDATE users SET is_admin = true WHERE username = 'your-username';

Project Structure

crypto-trust-feed/
├── src/                          # React frontend
│   ├── components/               # UI components (Twitter-style layout)
│   │   ├── Header.tsx            # Top bar: logo, search, trust score, admin
│   │   ├── LeftSidebar.tsx       # Expandable category tree
│   │   ├── RightSidebar.tsx      # New post, trust display, invites, trending
│   │   ├── PostFeed.tsx          # Trust-ranked post list
│   │   ├── PostCard.tsx          # Avatar + content + action bar
│   │   ├── PostDetail.tsx        # Full post + threaded comments + edit mode
│   │   ├── NewPostModal.tsx      # Category picker, textarea, image upload
│   │   ├── ImageLightbox.tsx     # Full-screen image overlay
│   │   ├── MobileNav.tsx         # Bottom bar (mobile only)
│   │   └── ProbationBanner.tsx   # "X boosted posts remaining"
│   ├── contexts/                 # React contexts
│   │   ├── AuthContext.tsx       # JWT in localStorage, login/signup/logout
│   │   └── ForumContext.tsx      # New post modal state
│   ├── hooks/                    # React Query hooks (one per API resource)
│   ├── pages/                    # Route pages (Index, Login, Signup, Profile, Admin)
│   └── lib/                      # API client, utilities
│
├── server/                       # Hono backend
│   └── src/
│       ├── index.ts              # Middleware stack, routes, cron setup
│       ├── db/
│       │   ├── schema.ts         # 8 Drizzle tables
│       │   ├── index.ts          # Postgres connection pool
│       │   └── seed.ts           # Category seeder
│       ├── lib/
│       │   ├── calibration.ts    # Trust engine: calibration + settlement + decay
│       │   └── env.ts            # Environment validation
│       ├── middleware/
│       │   ├── auth.ts           # JWT verify + admin guard
│       │   └── rate-limit.ts     # Sliding window rate limiter
│       └── routes/               # 11 route files
│
├── vite.config.ts                # Port 8080, API proxy to :3000
└── tailwind.config.ts            # Custom colors, fonts, animations

Commands

# Frontend
npm run dev           # Dev server (port 8080)
npm run build         # Production build
npm run test          # Run tests
npm run lint          # ESLint

# Backend
cd server
npm run dev           # Dev server (port 3000, auto-reload)
npx drizzle-kit generate  # Generate migration from schema changes
npx drizzle-kit migrate   # Apply migrations
npx tsx src/db/seed.ts     # Seed categories
npm run calibrate          # Manual trust calibration + settlement

Known Limitations

Things that don't work yet but should:

  • No pagination UI — backend supports ?page=, frontend only loads page 1
  • No post URL routing — posts open via React state, not shareable URLs
  • Comment votes don't persist — only post votes are tracked
  • Notifications are fake — bell icon is decorative
  • Sidebar links are dead — "Your Posts", "Settings" go nowhere
  • Share button is a no-op
  • Search is basic — ILIKE on content only, no ranking
  • Trending topics are hardcoded
  • No email verification
  • No delete confirmation dialogs
  • Report UI only sends "spam" — no reason picker

License

Private. All rights reserved.

About

Trust-weighted crypto forum. Your reach is earned through good judgment, not bought. Invite-only, dark-mode, Twitter-style UI. Full-stack TypeScript: React + Hono + PostgreSQL.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages