Building a Fantasy Stock Market for Social Media Creators
The idea is simple: what if social media creators were publicly traded stocks? Their follower count, engagement rate, and view velocity determine a base price. Player trading activity (buy pressure vs sell pressure) moves the price up or down. You start with a million dollars of virtual money and try to build the best portfolio.
Clout Investor is the implementation. React 19 frontend, Insforge BaaS backend with PostgreSQL and Deno edge functions, real-time data from YouTube and TikTok APIs, a quest system for retention, multiplayer mini-games for earning, and MySpace-style customizable profiles because nostalgia is a feature.
The Pricing Engine
This is the core mechanic. If the pricing feels wrong, the whole game feels wrong. Too volatile and it's random noise. Too stable and there's no reason to trade.
The formula:
MarketPrice = BasePrice x (1 + DemandFactor) x (1 + EventModifier) x VolatilityNoise
Base Price
Creator pricing uses a 7-tier system based on follower count:
| Followers | Price Range |
|---|---|
| 1 - 1K | $0.01 - $0.50 |
| 1K - 10K | $0.50 - $5 |
| 10K - 100K | $5 - $25 |
| 100K - 1M | $25 - $100 |
| 1M - 10M | $100 - $500 |
| 10M - 100M | $500 - $2K |
| 100M+ | $2K - $10K |
Within each tier, the price scales logarithmically. A creator with 500K followers doesn't sit at the midpoint of the $25-$100 range, they sit closer to $100 because follower growth is exponential.
Three factors weight the base price:
Tier position (60%): Where the creator sits within their tier.
Engagement multiplier (25%): Engagement rate maps to a 0.5x-2.0x multiplier. A creator with 10%+ engagement (exceptional for their size) gets a 1.7-2.0x boost. A creator with 0% engagement gets halved. This prevents dead accounts with millions of followers from being overpriced.
Views multiplier (15%): Views-to-followers ratio captures virality. A creator whose videos consistently get 20x their follower count in views is underpriced by followers alone. The views multiplier (0.8x to 1.5x) corrects for this.
Demand Factor
Player trading creates supply/demand pressure. The demand factor ranges from -0.50 to +0.50 (a 50% swing in either direction).
const imbalance = (buyVolume - sellVolume) / (buyVolume + sellVolume);
const volumeRatio = totalVolume / sharesOutstanding;
const volumeMultiplier = Math.min(2.0, Math.max(0.5, volumeRatio * 10));
const demandFactor = imbalance * 0.25 * volumeMultiplier;Volume matters. If only 1% of a creator's shares traded in the last 24 hours, the volume multiplier is low (0.5x) and buy/sell imbalance barely moves the price. If 10% of shares traded, the multiplier is high (2.0x) and the same imbalance creates a much larger swing. This mimics real markets where low-liquidity stocks are more stable (fewer trades) but more volatile per-trade.
The volume data lives in an in-memory cache (a Map<string, {buyVolume, sellVolume, lastReset}>), not the database. Price recalculation happens on every trade without a DB query. The cache resets every 24 hours.
Event Modifier
Admins can create market events that boost or nerf prices. Events target either all creators, a specific platform ("TikTok surge"), a specific creator ("Creator X goes viral"), or a category ("Gaming boom week"). Multipliers range from 0.5x to 2.0x and stack multiplicatively.
There's also a profit multiplier. When a player sells at a profit during an active event, the profit portion (not the principal) gets boosted. "Double profits week" means your $500 gain becomes $1,000, but your $500 loss stays $500. This creates FOMO pressure to trade during events, which is the whole point of events.
Volatility Noise
A small random factor (0.95 to 1.05) scaled by the coefficient of variation of recent prices. High-volatility creators get slightly more noise. This prevents prices from feeling mechanical.
Creator Data Integration
The game pulls real metrics from YouTube and TikTok. Each platform has an edge function (Deno, running on Insforge) that queries the relevant API.
YouTube: YouTube Data API v3. Returns subscriber count, video count, total views. Engagement rate is calculated as (totalViews / subscriberCount) / 100.
TikTok: Two APIs for reliability. ScrapTik handles search (finding creators by username). TikTok API23 (via RapidAPI) handles detailed user info (follower count, total likes, video count). If one API is down, the other still works for partial data.
TikTok's image CDN blocks direct requests from browsers (CORS). The workaround is wsrv.nl, a free image proxy service. Avatar URLs get rewritten to https://wsrv.nl/?url={encoded_tiktok_url}, which handles CORS, caching, and resizing.
Both APIs are called in parallel via Promise.all() when a user searches for a creator. Results are deduplicated by username and merged. A search for "MrBeast" returns his YouTube and TikTok profiles simultaneously.
Portfolio Math
Trade execution is straightforward but the cost basis tracking matters for the game to feel real:
// Buy
holdings[assetId].quantity += quantity;
holdings[assetId].avg_buy_price =
(oldQuantity * oldAvg + quantity * price) / newQuantity;
// Sell
const profit = (sellPrice - avgBuyPrice) * quantity;
if (profit > 0 && activeProfitMultiplier > 1) {
eventBonus = profit * (multiplier - 1);
}
balance += (quantity * sellPrice) + eventBonus;The portfolio summary calculates: total value, total cost, P&L (absolute and percentage), daily change, holdings count, and asset allocation breakdown (creator stocks vs index funds vs event contracts). All of this renders in Recharts: candlestick price charts, portfolio allocation pie charts, and 7-day performance lines.
Quest System
Retention requires a reason to come back every day. The quest system provides that.
Three tiers rotate on different schedules:
- Daily (3-5 quests, refresh every 24h): "Execute 3 trades", "Login today"
- Weekly (2-3 quests, refresh every 7 days): "Earn $50K profit", "Buy 5 different creators"
- Seasonal (1-2 quests, persist for the season): "Reach top 100 leaderboard", "Win 10 prediction contracts"
Quest types map to tracked actions: trades executed, profit earned, portfolio diversity (number of unique holdings), event contract wins, trending trades (buying a creator before they spike). Progress increments automatically when the relevant action happens.
The rotation system (rotate-quests.js edge function) selects from the quest pool, assigns to slot numbers, and tracks which quests each player has seen. Completed quests can't re-appear for the same rotation.
Multiplayer Earn Games
Two mini-games that let players earn virtual money outside of trading.
Line Rider (Plinko-Style Prediction)
A 10-column by 5-row grid. Players bet on which cell a ball will land in. Row multipliers are [7.6, 4.2, 2.9, 4.2, 7.6] (edges pay more, middle pays less, like real Plinko).
Round timing:
- 5 seconds: betting phase (place bets)
- 8 seconds: ball animation
- 2 seconds: results display
- 15 seconds per round total
The game is provably fair using seed-based commitment:
// Before round starts
round.seed_hash = SHA256(secret_seed); // Published to players
// After round ends
// Reveal the seed
// Players verify: SHA256(revealed_seed) === committed_hash
// Path is deterministic: generatePath(seed)
// No manipulation possibleBrokerage (Idle Clicker)
Click-based progression with exponential upgrade costs and auto-clicker unlocks. Earnings feed back into the player's trading balance. It's a low-effort engagement loop for players who want passive income between trades.
MySpace-Style Profiles
Every player gets a public profile page at /profile/{slug}. The customization is intentionally nostalgic:
- Custom bio
- Spotify track embed (paste a track URL, it embeds the player)
- Theme accent color and background color
- Background image URL
- Custom CSS (sandboxed to the profile container)
The custom CSS is the fun part. Players can write actual CSS that styles their profile. It's sandboxed (only applies within the profile component, can't escape to the rest of the app) but within that sandbox, anything goes. Gradient backgrounds, animated borders, custom fonts via @import.
Profile slugs are generated as username-{nanoid} for uniqueness while staying readable.
State Management
Zustand over Redux was an easy choice. Six stores, zero boilerplate:
useMarketStore: creator list, indexes, filtersusePlayerStore: current player profile, balance, net worthusePortfolioStore: holdings, orders, summaryuseEventStore: active events, contractsuseQuestStore: quests, achievements, progressuseUIStore: sidebar state, notifications
Each store is a flat object with setter functions. No reducers, no action types, no dispatch calls. Components subscribe to exactly the slice they need and only re-render when that slice changes. For a game with frequent state updates (price ticks, trade confirmations, quest progress), minimizing re-renders matters.
The Stack
Frontend: React 19.2 with TypeScript, Vite (rolldown compiler for faster builds), Tailwind CSS 3.4, Recharts 3.5 for financial charts, React Router v7, Zustand for state, React Query v5 for server state and caching.
Backend: Insforge BaaS with PostgreSQL. Edge functions in Deno for API integrations (YouTube, TikTok, Instagram, Twitter, Twitch). Authentication via Insforge Auth (OAuth + email/password). Real-time subscriptions via PostgreSQL triggers.
Deployment: Cloudflare Pages for the frontend (SPA with _redirects), Insforge cloud for the backend and edge functions.
The database has 20+ tables. The core ones: players, creators, creator_price_history (OHLC data), holdings, orders, events, event_contracts, contract_positions, quests, player_quests, quest_rotation, achievements, earn_rounds, earn_bets, indexes, index_components.
Investor Style Classification
The game classifies players into investing styles based on their behavior:
- Risk Taker: High volatility trades, frequent position changes
- Trend Chaser: Momentum plays, buys after price increases
- Long-Term Holder: Buy and hold, low turnover
- Event Specialist: Heavy prediction market activity
- Index Investor: Diversified, prefers indexes over individual creators
- Balanced: No dominant pattern
The classification shows on your profile as a badge. It's cosmetic but it creates identity and competitive differentiation.
What I'd Do Differently
The pricing engine should incorporate historical trends, not just current metrics. A creator who gained 500K followers this week should be priced differently than one who's been flat for months. Adding a momentum factor (rolling 7-day follower growth rate) would make prices more dynamic and reward players who spot trending creators early.
Index funds need rebalancing logic. Right now, index component weights are static. A "Top TikTok" index should automatically adjust weights as creator follower counts change. The rebalancing edge function is stubbed but not implemented.
The Insforge real-time subscriptions should drive price updates instead of polling. PostgreSQL triggers fire on creator_price_history inserts, but the frontend doesn't subscribe to them yet. Players currently see price changes on page refresh or when their own trade completes. A WebSocket connection carrying live price ticks would make the market feel alive.