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.
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.
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).
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.
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.
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.
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.
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.
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+2prevents 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.
This is a full-stack TypeScript application. No Python, no Java, no microservices — just two Node processes and a Postgres database.
| 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 |
| 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 |
- 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.
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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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.
- Node.js 18+
- PostgreSQL 14+
git clone https://github.com/simondice/crypto-trust-feed.git
cd crypto-trust-feed
npm install
cd server && npm install && cd ..CREATE USER cryptocunts WITH PASSWORD 'your-password';
CREATE DATABASE cryptocunts OWNER cryptocunts;# server/.env
DATABASE_URL=postgresql://cryptocunts:your-password@localhost:5432/cryptocunts
JWT_SECRET=your-secret-at-least-32-characters-long
PORT=3000cd server
npx drizzle-kit migrate # Create all 8 tables
npx tsx src/db/seed.ts # Seed 13 categoriesNo 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.
# Terminal 1 — backend (port 3000)
cd server && npm run dev
# Terminal 2 — frontend (port 8080, proxies API to backend)
npm run devOpen http://localhost:8080. Sign up, make yourself admin via SQL if needed:
UPDATE users SET is_admin = true WHERE username = 'your-username';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
# 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 + settlementThings 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
Private. All rights reserved.