62 KiB
Fractafrag — Full Platform Specification
Version: 1.1
Date: March 2026
Status: Pre-build handoff to GSD
Executive Summary
Fractafrag is a self-hosted web platform for creating, browsing, and generating GLSL shaders — real-time 2D and 3D visuals that run on the GPU.
It is three things fused: a TikTok-style adaptive feed of living, animated visuals that learns your taste from dwell time, votes, and replays; a Shadertoy-style in-browser code editor where anyone can write, fork, and publish shaders; and an AI generation layer where you describe what you want in plain language and the platform writes and renders the shader for you.
When a user wants something that doesn't exist yet — "ragdoll physics but dark and slow" — they post it to the desire queue. Other users or autonomous AI agents can fulfill it, with optional tips attached to high-demand requests.
The platform exposes a REST API and an MCP server so AI tools (Claude, GPT agents, custom automations) can browse, generate, and submit shaders programmatically — making Fractafrag a productive target for AI creative pipelines, not just a human browsing experience.
Everything runs in a single Docker Compose stack, self-hosted on Linux, no cloud dependencies required.
Monetization at launch is subscription tiers and AI generation credits. A full creator economy — revenue share, source licensing, commercial rights — is designed and documented in Section 11, but deliberately deferred until the platform has organic community traction. Build the data hooks now. Build the features later.
North star: make it fun first. Everything else follows.
Table of Contents
- Vision & Product Summary
- Tech Stack Decision
- Docker Compose Architecture
- Service Specs
- 4a. Frontend
- 4b. API
- 4c. MCP Server
- 4d. Renderer
- 4e. Worker
- Database Schema
- Authentication & Security
- AI Generation Pipeline
- Recommendation Engine
- Desire Queue & Bounty Board
- Monetization & Payment Tiers
- Creator Economy — Future Rollout
- MCP Server Tool Definitions
- API Endpoint Reference
- Parallel Build Tracks
- Milestone Roadmap
- Open Questions & Decisions for GSD
1. Vision & Product Summary
Fractafrag is a self-hosted, community-driven GLSL shader platform combining:
- A TikTok-style adaptive visual feed of live-rendered 2D/3D shaders
- A Shadertoy-style in-browser editor for creating and submitting shaders
- An AI generation system allowing users to prompt shaders into existence
- A desire queue and bounty board where users express what they want to see, and AI agents or human creators fulfill those requests
- A full REST API and MCP (Model Context Protocol) server enabling AI tools to browse, generate, and submit shaders autonomously
- A monetization layer built around AI generation credits, BYOK (bring your own key), and bounty facilitation fees
- A creator economy layer (future rollout — see Section 11) enabling revenue share, source licensing, and commercial rights for established creators
Primary focus for initial development: Build a platform that is genuinely fun to browse and use. The feed, the editor, the AI generation, and the desire queue are the product. Monetization features beyond the basic subscription and credit tiers are deliberately deferred until the platform has demonstrated organic community growth. All components should be architected with the future creator economy in mind, but not blocked on it.
What makes Fractafrag different from Shadertoy, GLSL Sandbox, and others:
- Adaptive feed — learns what you like from dwell time, votes, replays, and explicit desires
- AI generation built into the platform, not bolted on
- MCP server that lets any LLM-based agent plug in and autonomously generate or fulfill bounties
- Bounty/desire economy that closes the loop between user desire and content creation
- Fully self-hostable, Docker-native, no cloud dependency
Core name etymology:
fracta — fractal, generative, infinite mathematical form
frag — fragColor, the GLSL output variable every shader writes to. An easter egg for shader developers.
2. Tech Stack Decision
GSD should make final calls on language/framework, but the following is strongly recommended based on the platform's requirements:
Backend API
Recommendation: Python + FastAPI
Rationale:
- AI/ML integrations (pgvector embeddings, LLM clients, numpy for recommendation math) are Python-native
- FastAPI gives async performance comparable to Node without the callback hell
- Pydantic models double as API docs (auto-generates OpenAPI spec)
- Celery + Redis worker ecosystem is mature for the job queue
Alternative if team has stronger Node experience: Hono on Bun — fast, modern, TypeScript-native, handles the same async workloads well.
Frontend
React + Vite + Three.js / raw WebGL
Rationale:
- Three.js for 3D shader rendering in the editor
- Raw WebGL via canvas for the feed thumbnails (lighter, faster for many simultaneous renders)
- React for UI, TanStack Query for server state, Zustand for client state
- Tailwind CSS for styling
Database
- PostgreSQL — primary datastore (shaders, users, votes, bounties, transactions)
- pgvector extension — taste vectors, style embeddings, desire embeddings (nearest-neighbor queries)
- Redis — session cache, feed cache, rate limiting, BullMQ job queue
Renderer
- Headless Chromium via Puppeteer — runs shaders in an isolated browser context, captures video thumbnails and still previews
- Alternative worth evaluating: esbuild + offscreen-canvas in Node (lighter, but less accurate to browser rendering)
Payments
- Stripe — subscription tiers + one-time bounty payments
- Stripe Connect for creator payouts (bounty fulfillment)
3. Docker Compose Architecture
All services run within a single Docker Compose stack. The entire platform is self-contained with no external cloud dependencies required for core functionality (AI provider API keys are user-supplied).
fractafrag/
├── docker-compose.yml
├── docker-compose.override.yml # local dev overrides
├── .env.example
├── services/
│ ├── frontend/ # React app
│ ├── api/ # FastAPI backend
│ ├── mcp/ # MCP server
│ ├── renderer/ # Headless shader renderer
│ ├── worker/ # BullMQ/Celery job worker
│ └── nginx/ # Reverse proxy + SSL termination
├── db/
│ └── init.sql # Schema bootstrap
└── scripts/
└── seed.py # Dev seed data
docker-compose.yml (structure)
version: "3.9"
services:
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes: [./nginx/conf:/etc/nginx/conf.d, ./nginx/certs:/etc/ssl]
depends_on: [frontend, api, mcp]
frontend:
build: ./services/frontend
environment:
- VITE_API_URL=https://fractafrag.yourdomain.com/api
- VITE_MCP_URL=https://fractafrag.yourdomain.com/mcp
api:
build: ./services/api
environment:
- DATABASE_URL=postgresql://fracta:${DB_PASS}@postgres:5432/fractafrag
- REDIS_URL=redis://redis:6379
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- TURNSTILE_SECRET=${TURNSTILE_SECRET}
- JWT_SECRET=${JWT_SECRET}
- RENDERER_URL=http://renderer:3100
depends_on: [postgres, redis]
mcp:
build: ./services/mcp
environment:
- API_BASE_URL=http://api:8000
- MCP_API_KEY_SALT=${MCP_API_KEY_SALT}
ports: ["3200:3200"]
renderer:
build: ./services/renderer
shm_size: "512mb" # Required for Chromium
environment:
- MAX_RENDER_DURATION=8 # seconds
- OUTPUT_DIR=/renders
volumes: [renders:/renders]
worker:
build: ./services/worker
environment:
- DATABASE_URL=postgresql://fracta:${DB_PASS}@postgres:5432/fractafrag
- REDIS_URL=redis://redis:6379
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} # platform key for internal generation
- OPENAI_API_KEY=${OPENAI_API_KEY}
- RENDERER_URL=http://renderer:3100
depends_on: [postgres, redis, renderer]
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_USER=fracta
- POSTGRES_PASSWORD=${DB_PASS}
- POSTGRES_DB=fractafrag
volumes: [pgdata:/var/lib/postgresql/data]
redis:
image: redis:7-alpine
volumes: [redisdata:/data]
volumes:
pgdata:
redisdata:
renders:
Nginx Routing
/ → frontend (React SPA)
/api/* → api:8000 (FastAPI)
/mcp/* → mcp:3200 (MCP Server)
4. Service Specs
4a. Frontend
Stack: React 18, Vite, Three.js, TanStack Query, Zustand, Tailwind CSS
Pages & Routes:
| Route | Page | Description |
|---|---|---|
/ |
Feed | Infinite scroll adaptive shader feed |
/explore |
Explore | Browse by tag, trending, new, top |
/shader/:id |
Shader detail | Full-screen view, code, comments, vote |
/editor |
Editor | GLSL editor + live WebGL preview |
/editor/:id |
Fork editor | Edit existing shader |
/generate |
AI Generator | Prompt-to-shader interface |
/bounties |
Bounty board | Open desire queue |
/bounties/:id |
Bounty detail | Single desire, stats, submissions |
/profile/:username |
Profile | User shaders, votes, bounties |
/settings |
Settings | Account, API keys, subscription |
/login |
Auth | Login / register with CAPTCHA |
Key Frontend Components:
ShaderCanvas — Core WebGL renderer used everywhere. Accepts GLSL code, renders it live in a canvas element using WebGL2. Handles uniform injection (iTime, iResolution, iMouse, etc., Shadertoy-compatible). Used in the editor (full size), the feed (thumbnail/preview), and the shader detail page (full screen).
FeedItem — A feed card containing a looping ShaderCanvas thumbnail (auto-plays muted), title, author, vote controls (upvote/downvote), and dwell time tracking (IntersectionObserver + timer). Reports dwell time back to the API every 5 seconds while visible.
InfiniteFeed — Wraps FeedItem in a virtualized scroll list. Fetches next page of feed from /api/feed using cursor-based pagination. The feed endpoint returns shaders ranked by the recommendation engine for the authenticated user (or trending/new for anonymous users).
DesireInput — A natural language text input on the feed page. "Show me something more like X" — submits to the desire queue. Displays in the UI as a pending request with a loading state that resolves when fulfilled.
GLSLEditor — Monaco Editor (same as VS Code) with GLSL syntax highlighting, autocompletion for uniforms, and error underlining from the GLSL linter. Split pane: editor on left, live preview on right. Submit button triggers the validation + render pipeline.
BountyCard — Displays a desire/bounty. Shows original request text, heat score (how many users want something similar), tip amount if any, time posted, status (open/fulfilled), and a "Fulfill this" button for creators.
AiGeneratorPanel — The primary AI generation UI. Text prompt input, style sliders (chaos level, color temperature, motion type), provider selector (platform credits / BYOK), generation status, and the rendered result when complete. Shows remaining free generations or upgrade CTA.
Dwell Time Tracking:
The recommendation engine runs on signals. Dwell time is the most valuable. Implementation:
// FeedItem dwell tracking
const startTime = useRef(null);
const reported = useRef(0);
// IntersectionObserver fires when card enters/exits viewport
onVisible: () => { startTime.current = Date.now(); }
onHidden: () => {
const dwell = (Date.now() - startTime.current) / 1000;
if (dwell > 1) { // ignore flicker scrolls
api.reportDwell({ shaderId, dwell, replayed });
}
}
4b. API (FastAPI)
Base URL: /api/v1
Full endpoint reference in Section 12. High-level structure:
api/
├── main.py
├── routers/
│ ├── auth.py # register, login, refresh, logout
│ ├── shaders.py # CRUD, submit, fork, search
│ ├── feed.py # personalized feed, trending, new
│ ├── votes.py # upvote, downvote, dwell report
│ ├── generate.py # AI generation endpoints
│ ├── desires.py # desire queue, bounties
│ ├── bounties.py # bounty creation, fulfillment, payouts
│ ├── users.py # profiles, settings
│ ├── payments.py # Stripe webhooks, subscription, credits
│ └── mcp_keys.py # API key management for MCP clients
├── services/
│ ├── glsl_validator.py
│ ├── renderer_client.py
│ ├── embedding.py # style vector generation
│ ├── recommendation.py
│ ├── ai_generator.py
│ └── stripe_service.py
├── models/ # SQLAlchemy ORM models
├── schemas/ # Pydantic request/response schemas
└── middleware/
├── auth.py # JWT verification
└── rate_limit.py # Redis-backed rate limiting
Key Middleware:
- JWT authentication (all protected routes)
- Cloudflare Turnstile verification (auth endpoints + shader submit)
- Redis rate limiting (per IP + per user):
- Anonymous: 100 req/min
- Authenticated: 300 req/min
- MCP API key: configurable per tier
4c. MCP Server
Stack: Python using the official mcp SDK, HTTP + SSE transport
Port: 3200 (exposed via nginx at /mcp)
Transport: Streamable HTTP (production), stdio available for local dev
Auth: Bearer token (MCP API key) in Authorization header. Keys are issued per user account, managed in Settings → API Keys.
Full tool definitions in Section 11.
mcp/
├── server.py # MCP server entrypoint
├── tools/
│ ├── submit_shader.py
│ ├── browse_shaders.py
│ ├── get_desire_queue.py
│ ├── fulfill_desire.py
│ ├── get_trending.py
│ ├── tag_shader.py
│ └── get_feed_context.py
└── auth.py # API key verification via internal API call
Connection example for Claude Desktop:
{
"mcpServers": {
"fractafrag": {
"url": "https://fractafrag.yourdomain.com/mcp/sse",
"headers": {
"Authorization": "Bearer ff_key_your_api_key_here"
}
}
}
}
Trust tiers for MCP clients:
| Tier | Submissions/hour | Auto-publish | fulfill_desire | tag_shader |
|---|---|---|---|---|
| Probation (new key) | 10 | No (queued for review) | No | No |
| Trusted | 100 | Yes | Yes | Yes |
| Premium | 500 | Yes | Yes | Yes |
Promotion from Probation → Trusted: automatic after 10 approved submissions with >0.6 avg approval rate.
4d. Renderer Service
Stack: Node.js + Puppeteer (Headless Chromium)
Purpose: Accepts GLSL code via HTTP POST, renders it in an isolated browser context, returns a preview image and a short looping video (WebM/MP4).
Endpoint:
POST /render
{
"glsl": "void mainImage(out vec4 fragColor, in vec2 fragCoord) { ... }",
"duration": 5, // seconds to render
"width": 640,
"height": 360,
"fps": 30
}
Response:
{
"thumbnail_url": "/renders/abc123/thumb.jpg",
"preview_url": "/renders/abc123/preview.webm",
"duration_ms": 4823,
"peak_memory_mb": 112,
"error": null
}
Safety measures:
- Max render duration: 8 seconds (configurable)
- GPU memory cap: Chromium flag
--max-gum-memory-mb=256 - CPU timeout: process.kill after 15s regardless
- Sandboxed:
--no-sandboxdisabled in prod, use--sandboxwith user namespace isolation - Static analysis before render: detect infinite loops, banned extensions (compute shaders that could be GPU-abusive)
GLSL Validator (pre-render):
Run glslang (Khronos reference compiler) as a subprocess before sending to renderer. If lint fails, return error to submitter without spending a render.
echo "$GLSL_CODE" | glslangValidator --stdin -S frag
4e. Worker Service
Stack: Python + Celery (or BullMQ in Node if backend is Node) + Redis as broker
Jobs:
| Job | Trigger | Description |
|---|---|---|
render_shader |
On submission | Calls renderer, stores result, updates shader record |
embed_shader |
After render | Generates style embedding vector for recommendation |
process_desire |
New desire posted | Embeds desire, matches to existing shaders, optionally queues AI fulfillment |
ai_generate |
Manual or desire match | Calls LLM to write GLSL, submits via API |
payout_creator |
Bounty fulfilled + settled | Initiates Stripe Connect payout to creator |
expire_bounties |
Cron, daily | Marks old unfulfilled bounties as expired |
rebuild_feed_cache |
Cron, every 15min | Pre-computes trending/new lists for anonymous feed |
AI Generation Worker flow:
1. Dequeue job: { desire_id, prompt, style_metadata, provider, api_key }
2. Build system prompt (see Section 7)
3. Call LLM API (Anthropic / OpenAI / Ollama)
4. Extract GLSL from response
5. POST to /api/v1/shaders/submit (as platform bot user)
with fulfills_desire_id = desire_id
6. Wait for render result
7. If render passes: mark desire fulfilled, notify user
8. If render fails: retry with error feedback in next prompt (max 3 attempts)
9. If all retries fail: log failure, re-open desire
5. Database Schema
Core Tables
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user', -- user, moderator, admin
trust_tier TEXT NOT NULL DEFAULT 'standard', -- standard, creator, trusted_api
stripe_customer_id TEXT,
subscription_tier TEXT DEFAULT 'free', -- free, pro, studio
ai_credits_remaining INTEGER DEFAULT 0,
taste_vector vector(512), -- pgvector: learned taste embedding
created_at TIMESTAMPTZ DEFAULT NOW(),
last_active_at TIMESTAMPTZ
);
-- Shaders
CREATE TABLE shaders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID REFERENCES users(id),
title TEXT NOT NULL,
description TEXT,
glsl_code TEXT NOT NULL,
is_public BOOLEAN DEFAULT TRUE,
is_ai_generated BOOLEAN DEFAULT FALSE,
ai_provider TEXT, -- anthropic, openai, ollama, null
thumbnail_url TEXT,
preview_url TEXT,
render_status TEXT DEFAULT 'pending', -- pending, rendering, ready, failed
style_vector vector(512), -- pgvector: visual style embedding
style_metadata JSONB, -- { chaos_level, color_temp, motion_type, ... }
tags TEXT[],
shader_type TEXT DEFAULT '2d', -- 2d, 3d, audio-reactive
forked_from UUID REFERENCES shaders(id),
view_count INTEGER DEFAULT 0,
score FLOAT DEFAULT 0, -- cached hot score for feed ranking
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Votes
CREATE TABLE votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
shader_id UUID REFERENCES shaders(id),
value SMALLINT NOT NULL CHECK (value IN (-1, 1)),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (user_id, shader_id)
);
-- Engagement signals (dwell time, replays, shares)
CREATE TABLE engagement_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id), -- null for anonymous
session_id TEXT, -- anonymous session token
shader_id UUID REFERENCES shaders(id),
event_type TEXT NOT NULL, -- dwell, replay, share, generate_similar
dwell_secs FLOAT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Desires / Bounties
CREATE TABLE desires (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID REFERENCES users(id),
prompt_text TEXT NOT NULL,
prompt_embedding vector(512), -- embedded for similarity grouping
style_hints JSONB, -- { chaos_level, color_temp, etc }
tip_amount_cents INTEGER DEFAULT 0,
status TEXT DEFAULT 'open', -- open, in_progress, fulfilled, expired
heat_score FLOAT DEFAULT 1, -- updated as similar desires accumulate
fulfilled_by_shader UUID REFERENCES shaders(id),
fulfilled_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Similar desire grouping (many-to-many)
CREATE TABLE desire_clusters (
cluster_id UUID,
desire_id UUID REFERENCES desires(id),
similarity FLOAT,
PRIMARY KEY (cluster_id, desire_id)
);
-- Bounty tips (financial layer on top of desires)
CREATE TABLE bounty_tips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
desire_id UUID REFERENCES desires(id),
tipper_id UUID REFERENCES users(id),
amount_cents INTEGER NOT NULL,
stripe_payment_intent_id TEXT,
status TEXT DEFAULT 'held', -- held, released, refunded
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Payouts to creators
CREATE TABLE creator_payouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID REFERENCES users(id),
desire_id UUID REFERENCES desires(id),
gross_amount_cents INTEGER,
platform_fee_cents INTEGER, -- 10%
net_amount_cents INTEGER, -- 90%
stripe_transfer_id TEXT,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- API Keys (for MCP clients)
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
key_hash TEXT UNIQUE NOT NULL, -- bcrypt hash of the actual key
key_prefix TEXT NOT NULL, -- first 8 chars for display (ff_key_XXXXXXXX)
name TEXT, -- user-given label
trust_tier TEXT DEFAULT 'probation',
submissions_approved INTEGER DEFAULT 0,
rate_limit_per_hour INTEGER DEFAULT 10,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
-- AI generation log
CREATE TABLE generation_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
shader_id UUID REFERENCES shaders(id),
provider TEXT NOT NULL,
prompt_text TEXT,
tokens_used INTEGER,
cost_cents INTEGER, -- platform cost for credit-based generations
success BOOLEAN,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Comments
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shader_id UUID REFERENCES shaders(id),
author_id UUID REFERENCES users(id),
body TEXT NOT NULL,
parent_id UUID REFERENCES comments(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
Indexes
-- Feed performance
CREATE INDEX idx_shaders_score ON shaders(score DESC) WHERE is_public = TRUE;
CREATE INDEX idx_shaders_created ON shaders(created_at DESC) WHERE is_public = TRUE;
CREATE INDEX idx_shaders_tags ON shaders USING GIN(tags);
-- Recommendation (pgvector ANN)
CREATE INDEX idx_shaders_style_vector ON shaders
USING ivfflat (style_vector vector_cosine_ops) WITH (lists = 100);
CREATE INDEX idx_users_taste_vector ON users
USING ivfflat (taste_vector vector_cosine_ops) WITH (lists = 50);
CREATE INDEX idx_desires_embedding ON desires
USING ivfflat (prompt_embedding vector_cosine_ops) WITH (lists = 50);
-- Engagement
CREATE INDEX idx_engagement_user ON engagement_events(user_id, created_at DESC);
CREATE INDEX idx_engagement_shader ON engagement_events(shader_id, event_type);
-- Desires / bounties
CREATE INDEX idx_desires_status ON desires(status, heat_score DESC);
CREATE INDEX idx_desires_author ON desires(author_id);
6. Authentication & Security
User Auth Flow
- Registration: User submits email + password + Cloudflare Turnstile token
- Turnstile verification: API calls Cloudflare's
/siteverifyendpoint server-side. Request rejected if token invalid. - Password hashing: bcrypt with cost factor 12
- JWT issuance: Access token (15 min TTL) + refresh token (30 day TTL, stored in httpOnly cookie)
- Token refresh: Silent refresh on 401, using refresh token cookie
- Logout: Refresh token revoked in Redis blocklist
JWT Payload
{
"sub": "user-uuid",
"username": "johndoe",
"role": "user",
"tier": "free",
"iat": 1234567890,
"exp": 1234568790
}
Cloudflare Turnstile
- Widget type: Managed (invisible by default, visible challenge when risk is detected)
- Endpoints protected:
/register,/login,/shaders/submit - Server-side verification: POST to
https://challenges.cloudflare.com/turnstile/v0/siteverify - Turnstile is free and does not require Cloudflare proxying — works with direct DNS
MCP API Key Auth
- Keys generated as:
ff_key_+ 32 random bytes (base58 encoded) - Only key prefix stored in plaintext for display; full key bcrypt-hashed in DB
- Bearer token in Authorization header on every MCP request
- Rate limiting tracked per key in Redis:
ratelimit:apikey:{key_id}:hour
GLSL Sandboxing
- Static lint with glslang before any render
- Renderer process runs in Docker with no network access
- Chromium flags:
--disable-web-security=false,--no-first-run, memory limits - Max execution time enforced at renderer level (8s) and Puppeteer level (15s)
- Banned GLSL extensions list:
GL_ARB_compute_shader,GL_NV_*(GPU-specific, untestable)
7. AI Generation Pipeline
Providers Supported
- Anthropic (Claude) — claude-sonnet-4-5 recommended for GLSL generation quality
- OpenAI — gpt-4o or gpt-4.1
- Ollama (self-hosted) — any model with sufficient code generation capability (codellama, deepseek-coder recommended)
Provider Selection Logic
- If user has BYOK configured for a provider → use their key, no credits consumed
- If user has platform credits → use platform key, deduct credits based on tokens used
- If user has no credits → show upgrade CTA, block generation
System Prompt (GLSL Generation)
You are an expert GLSL shader developer specializing in WebGL fragment shaders.
Generate a single, complete GLSL fragment shader in Shadertoy-compatible format.
Rules:
- Entry point must be: void mainImage(out vec4 fragColor, in vec2 fragCoord)
- Available uniforms: iTime (float), iResolution (vec3), iMouse (vec4)
- Output must write to fragColor
- No external textures unless iChannel0 is specified in the request
- Shader must complete in under 16ms on mid-range GPU (avoid heavy loops)
- Do not use compute shaders or platform-specific extensions
Style guidance: {style_metadata}
User request: {prompt}
Respond with ONLY the GLSL code. No explanation, no markdown, no comments unless they
are GLSL // comments within the code itself.
Generation → Submission Flow
User submits prompt
↓
Credits check / BYOK validation
↓
Enqueue ai_generate job (Redis/Celery)
↓ (async, user sees "generating..." state via SSE or polling)
Worker: build prompt → call LLM API → extract GLSL
↓
Worker: POST /api/v1/shaders/internal-submit (bot token)
↓
API: glslang lint
↓ (fail → retry with error in prompt, max 3 attempts)
API: POST /render to renderer service
↓
Renderer: Headless Chrome renders shader → returns thumbnail + preview video
↓
API: store shader record + media URLs + style embedding
↓
API: notify user via SSE (or frontend polls /api/v1/generate/status/{job_id})
↓
Frontend: shader appears in user's feed + editor
Retry with Error Feedback
If the generated GLSL fails lint or render, the worker appends the error to the next prompt:
Previous attempt failed with this GLSL compiler error:
{glslang_error_output}
Please fix the shader. Here is the failing code:
{previous_glsl}
Corrected shader (GLSL only):
BYOK (Bring Your Own Key)
Users can store their own API keys in Settings → AI Keys. Keys are encrypted at rest using AES-256-GCM with a server-side key derived from their user ID + a master secret. Decrypted only in the worker context when a job runs. Never returned to the client.
8. Recommendation Engine
The feed is the heart of the product. Every decision in the recommendation engine should optimize for "shaders this user will watch longer and vote on."
Taste Vector
Each user has a 512-dimension taste_vector stored in pgvector. It is updated incrementally as they interact with the platform.
Signal weights:
| Signal | Weight |
|---|---|
| Upvote | +1.0 |
| Downvote | -0.8 |
| Dwell > 10s (no upvote) | +0.3 |
| Dwell > 30s (no upvote) | +0.6 |
| Replay | +0.5 |
| Explicit desire submission | +1.2 |
| Skip < 2s | -0.4 |
Update formula (exponential moving average):
new_taste = alpha * shader_style_vector * weight + (1 - alpha) * current_taste_vector
# alpha = 0.15 (responsive but not volatile)
Style Embedding (per shader):
At submission time, the worker calls a lightweight classifier (fine-tuned embedding model or GPT-4o with structured output) to extract style metadata and produce a 512-dim embedding:
{
"chaos_level": 0.85,
"color_temperature": "cool",
"motion_type": "fluid",
"geometry": "abstract_2d",
"complexity": "high",
"palette": ["#1a1a2e", "#0f3460", "#533483"]
}
This metadata is also human-readable and exposed in the UI on each shader card.
Feed Ranking Query
SELECT s.id, s.title, s.preview_url, s.score,
1 - (s.style_vector <=> $1::vector) AS similarity
FROM shaders s
WHERE s.is_public = TRUE
AND s.render_status = 'ready'
AND s.id NOT IN (
SELECT shader_id FROM engagement_events
WHERE user_id = $2 AND created_at > NOW() - INTERVAL '30 days'
)
ORDER BY
(similarity * 0.6 + s.score * 0.3 + RANDOM() * 0.1) DESC
LIMIT 20
OFFSET $3;
-- $1 = user's taste_vector
-- $2 = user_id
-- $3 = cursor offset
The 10% random factor prevents the feed from becoming a filter bubble.
Anonymous users: Served from a pre-computed trending/new cache (rebuilt every 15 minutes by the worker cron).
Hot Score (for trending)
# Wilson score lower bound for vote confidence + time decay
def hot_score(upvotes, downvotes, age_hours):
n = upvotes + downvotes
if n == 0:
return 0
z = 1.96 # 95% confidence
p = upvotes / n
wilson = (p + z*z/(2*n) - z * sqrt(p*(1-p)/n + z*z/(4*n*n))) / (1 + z*z/n)
decay = 1 / (1 + age_hours / 48) # halves every 2 days
return wilson * decay
Score is recomputed on every vote and cached on the shader record.
9. Desire Queue & Bounty Board
Desire Submission Flow
- User types a desire in the FeedInput or BountyBoard page
- Frontend POST to
/api/v1/desires - API stores desire, embeds the text (vector), checks for similar existing desires
- If similar desires exist (cosine similarity > 0.82): cluster them, update heat score
- Enqueue
process_desireworker job - Worker decides: auto-fulfill via AI immediately (if heat score low, queue not backed up) or list on bounty board for human/AI fulfillment
Desire Status Lifecycle
open → in_progress → fulfilled
↓
expired (if no fulfillment within TTL)
Bounty Board (Human + AI Fulfillment)
The bounty board at /bounties shows open desires sorted by heat score (demand). Any user or AI client can view and fulfill a bounty.
Human fulfillment:
- Creator opens a bounty, opens the editor, writes a shader
- On submit, selects "fulfills bounty #ID"
- After render passes, desire is marked fulfilled, tip released to creator (90%) and platform (10%)
AI fulfillment (via MCP):
# Example autonomous agent loop
desires = mcp.call("get_desire_queue", { "min_heat": 3, "limit": 5 })
for desire in desires:
shader_code = llm.generate_glsl(desire["prompt_text"])
result = mcp.call("submit_shader", {
"glsl_code": shader_code,
"title": f"Generated: {desire['prompt_text'][:40]}",
"tags": ["ai-generated"],
"fulfills_desire_id": desire["id"]
})
Tip Payout Timing:
- Tips are held in Stripe escrow when the bounty is posted
- Released 48 hours after fulfillment (dispute window)
- If user marks as "not what I wanted" within 48h, tip is refunded and desire re-opened
Heat Score
def update_heat_score(desire_id):
# Count similar desires posted in last 7 days
similar_count = db.query("""
SELECT COUNT(*) FROM desires
WHERE status = 'open'
AND created_at > NOW() - INTERVAL '7 days'
AND 1 - (prompt_embedding <=> $1) > 0.82
""", desire.prompt_embedding)
# Heat = log(similar_count + 1) * recency_factor
heat = log(similar_count + 1) * (1 / (1 + age_hours / 24))
db.update_desire(desire_id, heat_score=heat)
10. Monetization & Payment Tiers
Subscription Tiers
| Feature | Free | Pro ($12/mo) | Studio ($39/mo) |
|---|---|---|---|
| Browse feed | ✓ | ✓ | ✓ |
| Vote & comment | ✓ | ✓ | ✓ |
| Manual GLSL submissions | 5/month | Unlimited | Unlimited |
| AI generations (platform credits) | 0 | 50/month | 200/month |
| BYOK (own API key) | ✗ | ✓ | ✓ |
| MCP API access | Read-only | ✓ (Probation tier) | ✓ (Trusted tier) |
| Bounty posting | ✗ | ✓ | ✓ |
| Bounty fulfillment (creator payouts) | ✗ | ✓ | ✓ |
| Private shaders | ✗ | ✓ | ✓ |
| Commercial license downloads | Per-shader | 10/month included | 50/month included |
AI Credit Packs (one-time, no subscription required)
- 20 generations — $4
- 100 generations — $16
- 500 generations — $60
Bounty Platform Fee
- 10% of tip amount collected when a bounty is fulfilled
- Stripe Connect used for creator payouts (handles tax forms, international transfers)
- Minimum tip: $1.00
- Minimum payout: $10.00 (held until threshold reached)
BYOK Mechanics
- Free tier: BYOK not available (prevents platform abuse with no revenue)
- Pro/Studio: User adds their Anthropic/OpenAI key in Settings
- Key stored encrypted, used only in worker context
- No platform credits consumed; user pays their provider directly
- Usage is still logged (token counts, model) for their own reference
Stripe Integration
Subscription: Stripe Billing with monthly/annual billing, webhook events handled at /api/v1/payments/webhook
One-time credits: Stripe Payment Intents, credit balance updated on payment_intent.succeeded webhook
Creator payouts: Stripe Connect Express accounts. Creators onboard via Stripe Connect flow. Platform transfers net_amount to their Connect account on bounty settlement.
Key webhook events to handle:
customer.subscription.created/updated/deletedpayment_intent.succeededtransfer.created(payout confirmation)account.updated(Connect onboarding complete)
11. Creator Economy — Future Rollout
⚠️ Rollout Note — Read Before Building
Everything in this section is intentionally deferred. The initial development focus is on building a platform that is genuinely fun to browse, fast to use, and compelling enough that creators want to contribute without any financial incentive. A monetization system sitting on top of a platform nobody enjoys is worthless.
Do not build any of this during M0–M5. The exception is the data instrumentation noted in section 11f — certain engagement tracking fields and schema stubs should be put in place from day one so the architecture does not need to be reconstructed when the time comes. Build the hooks, not the features.
Revisit this section when the platform has reached meaningful organic usage — sustained daily active users, a growing shader library, and real community behavior. That is the signal that monetization will reinforce rather than replace organic growth.
11a. Design Philosophy
The Fractafrag creator economy is built on one principle: reward shaders that people actually spend time with. Not shaders that game an algorithm, not shaders that get one viral spike — shaders with sustained engagement over time.
Every signal needed to measure this is already being collected by the recommendation engine (dwell time, replays, votes, desire fulfillments). The creator economy is simply a financial layer on top of data that already exists. This is why the instrumentation can be stubbed now without building the full system.
All shaders remain freely browsable and playable regardless of monetization status. The rendered output is always public. What creators can optionally monetize is access to the underlying GLSL source code and commercial usage rights. The feed never discriminates on monetization status — a source-locked shader ranks identically to an open one.
11b. Shader Access Tiers
Creators choose one of three modes per shader, configurable at submit time or changed later from the shader settings page.
Open (default) Full source visible, forkable, free. The community layer. Earns passive revenue share from the subscription pool based on engagement. This should remain the dominant mode — the majority of shaders should always be open.
Source-locked The rendered output plays freely everywhere — feed, detail page, embeds. The GLSL source code tab is replaced with a paywall. Creator sets a one-time unlock price (minimum $2, maximum $200). A buyer pays once and gets permanent personal-use access to the source. They can read it, learn from it, use it in personal projects. They cannot redistribute or resell it.
Commercial license An add-on layer that sits on top of either Open or Source-locked. Creator sets a separate commercial license price. Purchasing it grants rights to use the shader in live performances, music videos, brand installations, broadcast, or any revenue-generating context. Typically priced at 3–10x the source-unlock price by the creator.
| Open | Source-locked | Commercial license | |
|---|---|---|---|
| Render plays in feed | ✓ | ✓ | ✓ |
| Source code visible | ✓ | Paywall | Paywall (unless also unlocked) |
| Forkable | ✓ | After unlock | After unlock |
| Personal use | ✓ | ✓ | ✓ |
| Commercial use | ✗ | ✗ | ✓ |
| Platform cut | 0% | 10% | 10% |
| Creator cut | 100% of rev share | 90% of sale | 90% of sale |
11c. Subscription Revenue Share Pool
A percentage of monthly subscription revenue is pooled and distributed to creators proportional to their engagement contribution that month.
Pool size: 20% of gross subscription revenue (Pro + Studio subscribers)
Engagement score per shader per month:
score =
(total_dwell_seconds x 1.0)
+ (replay_count x 30)
+ (upvote_count x 60)
+ (desire_fulfillments x 120)
+ (source_unlocks x 0) <- already compensated separately
Distribution formula:
creator_monthly_payout =
pool_total x (sum of creator shader scores / sum of all shader scores)
Example at modest scale:
- Monthly subscription revenue: $3,000
- Pool (20%): $600
- Creator shaders drove 4% of total platform engagement
- Creator earns: $24
Small at first. Meaningful at scale. Fully passive — creators earn it just by making things people watch.
Minimum payout threshold: $10 (held until reached, released via Stripe Connect). Paid on the 15th of the following month for the previous full calendar month.
11d. Verified Creator Program
Unlocks source-lock and commercial license features, plus elevated revenue share weighting.
Thresholds to reach Verified status:
- Minimum 10 approved shaders on the platform
- Minimum 30-day account age
- Minimum engagement score of 500 in any single month
- No active moderation flags
Benefits:
- Can set source-lock pricing on new and existing shaders
- Can set commercial license pricing
- 1.2x multiplier on revenue share pool weighting
- Featured placement in Explore page creator spotlight (rotates weekly)
- Verified badge on profile and shader cards
- Access to creator analytics dashboard
Verified status is reviewed quarterly. Accounts with moderation actions or sustained low engagement can be downgraded.
11e. Creator Analytics Dashboard
A dedicated /dashboard route for Verified creators showing:
- This month: Estimated earnings, source unlock revenue, commercial license revenue, total engagement score, rank percentile
- Per shader: Dwell time, replay count, vote ratio, unlock count, license count, estimated pool contribution
- Payout history: Past payouts, pending balance, Stripe Connect status
- Desire fulfillments: Bounties fulfilled, tips received, tip payout history
The dashboard should make the connection between creative output and financial return extremely legible. Creators should be able to see exactly which shaders are earning and why.
11f. Data Instrumentation to Build Now (M0–M5)
These schema additions and API stubs should be included in the initial build. They are cheap to add early and expensive to retrofit later. None of the actual payment or payout logic should be implemented — only the structure.
Schema stubs to include in initial DB migration:
-- Add to shaders table (all nullable/defaulted, not enforced until creator economy launches)
ALTER TABLE shaders ADD COLUMN access_tier TEXT DEFAULT 'open';
ALTER TABLE shaders ADD COLUMN source_unlock_price_cents INTEGER;
ALTER TABLE shaders ADD COLUMN commercial_license_price_cents INTEGER;
ALTER TABLE shaders ADD COLUMN verified_creator_shader BOOLEAN DEFAULT FALSE;
-- Source unlock purchase records (dormant until activated)
CREATE TABLE source_unlocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shader_id UUID REFERENCES shaders(id),
buyer_id UUID REFERENCES users(id),
license_type TEXT NOT NULL, -- personal, commercial
amount_cents INTEGER NOT NULL,
platform_fee_cents INTEGER NOT NULL,
stripe_payment_intent_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Monthly engagement snapshots for revenue share calculation (dormant)
CREATE TABLE creator_engagement_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID REFERENCES users(id),
month DATE NOT NULL,
total_score FLOAT NOT NULL,
pool_share FLOAT,
payout_cents INTEGER,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Verified creator status fields
ALTER TABLE users ADD COLUMN is_verified_creator BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN verified_creator_at TIMESTAMPTZ;
ALTER TABLE users ADD COLUMN stripe_connect_account_id TEXT;
Engagement tracking already in the spec — no changes needed:
engagement_eventstable (dwell, replay, share) ✓votestable (upvote/downvote) ✓desirestable (fulfillment tracking) ✓
API endpoints to stub as 501 Not Implemented:
GET /api/v1/dashboard <- creator dashboard data
GET /api/v1/shaders/:id/unlock-status <- check if user has unlocked source
POST /api/v1/shaders/:id/unlock <- purchase source unlock
POST /api/v1/shaders/:id/commercial <- purchase commercial license
POST /api/v1/me/creator/apply <- apply for verified status
GET /api/v1/me/creator/earnings <- earnings history
Stubbing these at 501 means the frontend can reference them, MCP tool specs can document them, and the architecture is coherent — without any payment logic being live.
11g. Rollout Trigger Criteria
Begin active development of the creator economy when all three of the following are true:
- The platform has sustained 500+ daily active users for at least 30 consecutive days
- The shader library has 1,000+ approved shaders with genuine style diversity
- At least 20 creators have submitted 5 or more shaders organically without financial incentive
At that point the community exists, the content exists, and monetization will accelerate existing behavior rather than try to manufacture it.
12. MCP Server Tool Definitions
All tools communicate via JSON-RPC 2.0 over HTTP+SSE. The MCP server proxies calls to the internal API.
submit_shader
Submit a new GLSL shader to the platform.
{
"name": "submit_shader",
"description": "Submit a GLSL fragment shader to Fractafrag. The shader will be linted, rendered, and added to the feed if it passes validation.",
"inputSchema": {
"type": "object",
"required": ["glsl_code", "title"],
"properties": {
"glsl_code": { "type": "string", "description": "Complete GLSL fragment shader code, Shadertoy-compatible" },
"title": { "type": "string", "maxLength": 120 },
"description": { "type": "string", "maxLength": 1000 },
"tags": { "type": "array", "items": { "type": "string" }, "maxItems": 10 },
"shader_type": { "type": "string", "enum": ["2d", "3d", "audio-reactive"], "default": "2d" },
"style_metadata": {
"type": "object",
"properties": {
"chaos_level": { "type": "number", "minimum": 0, "maximum": 1 },
"color_temperature": { "type": "string", "enum": ["warm", "cool", "neutral", "monochrome"] },
"motion_type": { "type": "string" }
}
},
"fulfills_desire_id": { "type": "string", "format": "uuid", "description": "If this shader fulfills an open bounty, include the desire ID" }
}
}
}
browse_shaders
Search and browse shaders on the platform.
{
"name": "browse_shaders",
"description": "Browse and search shaders on Fractafrag",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } },
"sort": { "type": "string", "enum": ["trending", "new", "top", "random"], "default": "trending" },
"shader_type": { "type": "string", "enum": ["2d", "3d", "audio-reactive", "all"], "default": "all" },
"limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 },
"cursor": { "type": "string" }
}
}
}
get_desire_queue
Retrieve open desires/bounties from the queue.
{
"name": "get_desire_queue",
"description": "Get open shader desires/bounties sorted by demand heat score",
"inputSchema": {
"type": "object",
"properties": {
"min_heat": { "type": "number", "minimum": 0, "default": 0, "description": "Minimum heat score (higher = more demand)" },
"has_tip": { "type": "boolean", "description": "Filter to only tipped bounties" },
"min_tip_cents": { "type": "integer" },
"limit": { "type": "integer", "minimum": 1, "maximum": 20, "default": 10 }
}
}
}
fulfill_desire
Mark a desire as fulfilled by an existing shader.
{
"name": "fulfill_desire",
"description": "Mark an open desire as fulfilled by a shader you've submitted",
"inputSchema": {
"type": "object",
"required": ["desire_id", "shader_id"],
"properties": {
"desire_id": { "type": "string", "format": "uuid" },
"shader_id": { "type": "string", "format": "uuid" }
}
}
}
get_trending
Get currently trending shaders and platform stats for context.
{
"name": "get_trending",
"description": "Get trending shaders and platform activity context",
"inputSchema": {
"type": "object",
"properties": {
"period": { "type": "string", "enum": ["1h", "24h", "7d"], "default": "24h" },
"limit": { "type": "integer", "default": 10 }
}
}
}
get_feed_context
Get personalized feed context for a user (for AI agents acting on behalf of a user).
{
"name": "get_feed_context",
"description": "Get the current feed context: what's trending, what style gaps exist, top open desires",
"inputSchema": {
"type": "object",
"properties": {
"include_taste_profile": { "type": "boolean", "default": false }
}
}
}
tag_shader
Add or update tags on a shader (Trusted tier only).
{
"name": "tag_shader",
"description": "Add tags to a shader. Trusted API clients only.",
"inputSchema": {
"type": "object",
"required": ["shader_id", "tags"],
"properties": {
"shader_id": { "type": "string", "format": "uuid" },
"tags": { "type": "array", "items": { "type": "string" }, "maxItems": 10 }
}
}
}
13. API Endpoint Reference
Auth
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/register |
Register new user | Public + Turnstile |
| POST | /api/v1/auth/login |
Login, returns JWT | Public + Turnstile |
| POST | /api/v1/auth/refresh |
Refresh access token | Refresh cookie |
| POST | /api/v1/auth/logout |
Revoke refresh token | JWT |
Shaders
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/shaders |
List/search shaders | Public |
| GET | /api/v1/shaders/:id |
Get shader | Public |
| POST | /api/v1/shaders |
Submit new shader | JWT + Turnstile |
| PUT | /api/v1/shaders/:id |
Update shader | JWT (owner) |
| DELETE | /api/v1/shaders/:id |
Delete shader | JWT (owner/admin) |
| POST | /api/v1/shaders/:id/fork |
Fork shader | JWT |
Feed
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/feed |
Personalized feed | JWT (or anon) |
| GET | /api/v1/feed/trending |
Trending feed | Public |
| GET | /api/v1/feed/new |
New shaders feed | Public |
| POST | /api/v1/feed/dwell |
Report dwell time signal | JWT / session |
Votes & Engagement
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/shaders/:id/vote |
Upvote or downvote | JWT |
| DELETE | /api/v1/shaders/:id/vote |
Remove vote | JWT |
| POST | /api/v1/shaders/:id/replay |
Report replay signal | JWT / session |
AI Generation
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/generate |
Start AI generation job | JWT |
| GET | /api/v1/generate/status/:job_id |
Poll job status | JWT |
| GET | /api/v1/generate/credits |
Check credit balance | JWT |
Desires & Bounties
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/desires |
List open desires | Public |
| POST | /api/v1/desires |
Post a desire | JWT (Pro+) |
| GET | /api/v1/desires/:id |
Get desire detail | Public |
| POST | /api/v1/desires/:id/fulfill |
Fulfill a desire | JWT |
| POST | /api/v1/desires/:id/tip |
Add tip to bounty | JWT (Pro+) |
Users & Settings
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/users/:username |
Public profile | Public |
| GET | /api/v1/me |
Own profile + settings | JWT |
| PUT | /api/v1/me |
Update settings | JWT |
| GET | /api/v1/me/api-keys |
List API keys | JWT |
| POST | /api/v1/me/api-keys |
Create API key | JWT (Pro+) |
| DELETE | /api/v1/me/api-keys/:id |
Revoke API key | JWT |
| PUT | /api/v1/me/ai-keys |
Update BYOK keys | JWT (Pro+) |
Payments
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/payments/checkout |
Create Stripe checkout session | JWT |
| POST | /api/v1/payments/webhook |
Stripe webhook handler | Stripe sig |
| GET | /api/v1/payments/portal |
Stripe customer portal URL | JWT |
| POST | /api/v1/payments/credits |
Purchase credit pack | JWT |
| POST | /api/v1/payments/connect/onboard |
Start creator Connect onboarding | JWT |
14. Parallel Build Tracks
GSD can run these tracks in parallel. Dependencies are noted — do not start a track that depends on an incomplete track.
Track A: Infrastructure & Data Layer
Can start immediately. No dependencies.
- Docker Compose scaffold with all services defined
- PostgreSQL container with pgvector extension
- Redis container
- Database migrations (schema from Section 5)
.env.examplewith all required vars documented- Nginx reverse proxy config (HTTP + SSL termination)
- CI/CD: basic health check endpoints on all services
Deliverable: docker compose up runs all services, DB schema bootstraps, all containers healthy.
Track B: Auth & User System
Depends on: Track A (DB running)
Can run parallel with: C, D, E
- User registration + bcrypt password hashing
- Cloudflare Turnstile integration (server-side verification)
- JWT issuance + refresh token flow (httpOnly cookie)
- Redis refresh token blocklist (for logout)
/api/v1/auth/*endpoints/api/v1/meprofile + settings endpoints- API key generation + management (
/api/v1/me/api-keys)
Deliverable: Users can register, login, refresh, logout. API keys can be created and listed.
Track C: Shader Submission & Renderer
Depends on: Track A (DB + Docker network)
Can run parallel with: B, D, E
- GLSL validator (glslang integration)
- Renderer service (Puppeteer + headless Chromium)
- Render endpoint: POST
/render→ returns thumbnail + preview video /api/v1/shadersCRUD endpoints- Shader submission pipeline: validate → render → store → embed
- Style metadata extractor (simple heuristic first, LLM-based later)
- Style embedding generation (pgvector insert)
- Media storage: rendered thumbnails + previews to Docker volume (or S3-compatible if configured)
Deliverable: Shaders can be submitted via API, pass lint, render in headless Chromium, and are stored with thumbnails.
Track D: Frontend Shell
Depends on: Track B (auth endpoints live)
Can run parallel with: C, E
- React + Vite project scaffold
- Tailwind CSS setup
- Route structure (all pages as stubs)
- Auth flows: login, register, JWT storage, refresh interceptor
- ShaderCanvas component (WebGL, Shadertoy-compatible uniforms)
- GLSL Editor (Monaco + GLSL syntax highlighting)
- Basic shader submission UI (editor → submit → status polling)
- User settings page (account, subscription placeholder)
Deliverable: Users can log in, open the editor, write GLSL, see a live preview, and submit.
Track E: MCP Server
Depends on: Track B (API key auth), Track C (shaders submittable)
Can run parallel with: D, F
- MCP server scaffold (Python
mcpSDK, HTTP+SSE transport) - API key verification middleware (calls internal API)
submit_shadertoolbrowse_shaderstoolget_trendingtoolget_desire_queuetool (returns empty until Track G)fulfill_desiretool (stub until Track G)get_feed_contexttooltag_shadertool (trust tier gated)- Rate limiting per key (Redis)
- Trust tier enforcement
Deliverable: Claude Desktop (or any MCP client) can connect, browse shaders, and submit new ones.
Track F: Feed & Recommendation Engine
Depends on: Track C (style vectors being generated), Track B (users exist)
Can run parallel with: E, G
- Hot score calculation on vote events
- Anonymous feed: trending + new (pre-computed cache)
- Authenticated feed: taste vector query (pgvector ANN)
- Dwell time reporting endpoint + taste vector update logic
- Vote endpoints (upvote/downvote) + score recalculation
- Feed API endpoint with cursor pagination
- Frontend: InfiniteFeed component with virtualization
- Frontend: FeedItem with dwell tracking (IntersectionObserver)
- Frontend: vote controls
Deliverable: Logged-in users see a personalized feed that improves with engagement.
Track G: Desire Queue & Bounty Board
Depends on: Track B (auth), Track C (shader submission), Track F (feed exists)
Can run parallel with: H
- Desire submission endpoint + embedding
- Similarity clustering (pgvector cosine search)
- Heat score calculation
- Bounty board API endpoints
- Desire fulfillment flow (link shader → desire → mark fulfilled)
- Frontend: DesireInput component on feed page
- Frontend: Bounty board page + BountyCard components
- Worker:
process_desirejob - Tip holding (basic — Stripe integration in Track H)
Deliverable: Users can post desires, see the bounty board, and fulfill bounties with manually-written shaders.
Track H: Payments & Monetization
Depends on: Track B (users), Track G (bounties exist)
Can run parallel with: I
- Stripe account + webhook setup
- Subscription checkout flow (Free / Pro / Studio)
- Webhook handler for subscription events
- Subscription tier enforcement in API middleware
- Credit pack purchase flow
- Credit deduction on AI generation (Track I)
- Stripe Connect creator onboarding flow
- Bounty tip escrow on desire post
- Bounty tip release on fulfillment (48h settle)
- Payout initiation (Stripe Connect transfer)
- Frontend: upgrade modals, credit balance display, settings/billing page
Deliverable: Users can subscribe, purchase credits, and creators can receive payouts for fulfilled bounties.
Track I: AI Generation
Depends on: Track C (renderer), Track B (auth + BYOK key storage), Track H (credit system)
Final track — start last
- BYOK key storage (encrypted at rest)
- AI provider clients: Anthropic, OpenAI, Ollama
- System prompt template + style injection
- Generation job (Celery/BullMQ worker)
- Retry logic with error feedback (up to 3 attempts)
- Job status endpoint (SSE or polling)
- Frontend: AiGeneratorPanel component
- Frontend: generation status UI (streaming if possible)
- Credit deduction logic (tokens used → credits consumed)
- Generation log for user history
Deliverable: Users can type a prompt, watch a shader generate, and see it appear in their feed.
15. Milestone Roadmap
M0 — Foundation (Tracks A + B)
Goal: Infrastructure live, auth working, can create an account and log in.
Estimated effort: 1–2 weeks parallel dev
M1 — Core Shader Loop (Tracks C + D)
Goal: Can write, submit, and view a GLSL shader. Live feed of manually submitted shaders.
Estimated effort: 2–3 weeks
First dogfood milestone — platform is usable for basic shader sharing.
M2 — Intelligence Layer (Tracks E + F)
Goal: MCP server live (AI clients can connect), personalized feed working.
Estimated effort: 2 weeks
Key milestone — the platform starts feeling different from Shadertoy.
M3 — Desire Economy (Track G)
Goal: Users can post desires, bounty board is live, human fulfillment works.
Estimated effort: 1–2 weeks
M4 — Monetization (Track H)
Goal: Subscriptions, credits, and creator payouts all functional.
Estimated effort: 2 weeks
Revenue-ready milestone.
M5 — AI Generation (Track I)
Goal: Platform-powered AI generation with credits + BYOK. Autonomous MCP agents can fulfill bounties.
Estimated effort: 2 weeks
Full product milestone. Everything is live.
16. Open Questions & Decisions for GSD
The following require a decision before or during implementation. Flagged here so GSD doesn't have to guess.
Q1: Media storage for renders
Render thumbnails and preview videos can be stored on the Docker volume (simple, works for single-server) or in an S3-compatible bucket (Minio self-hosted, or AWS S3). Volume is simpler to start; S3 is better if storage grows large.
→ Recommendation: Start with Docker volume, add S3 config flag for later migration.
Q2: Style embedding model
The style vector for each shader needs to be generated. Options:
- Heuristic classifier (fast, cheap, less accurate) — parse GLSL for math patterns, color functions, etc.
- LLM with structured output (accurate, slower, costs tokens)
- Fine-tuned embedding model (best, requires training data we don't have yet)
→ Recommendation: Start with heuristic + LLM structured output for M1. Fine-tune later.
Q3: Renderer fidelity vs. speed
Puppeteer/Chromium renders accurately but is heavy. A Node.js offscreen-canvas approach is lighter but may render differently than users' browsers.
→ Recommendation: Puppeteer for M1 for accuracy. Profile performance at M2, optimize if needed.
Q4: SSE vs polling for generation status
AI generation takes 5–30 seconds. User needs to see progress. Options:
- Server-Sent Events (real-time, slightly more complex)
- Polling every 2 seconds (simpler, slightly worse UX)
→ Recommendation: Polling for M5 (simpler), SSE upgrade in a follow-up sprint.
Q5: Ollama endpoint config
Self-hosted Ollama requires the user to provide both their API key (blank for Ollama) and the endpoint URL. The settings UI needs to handle this. Confirm UX approach before building BYOK settings page.
Q6: Comment system scope at M1
Comments on shaders are in the schema but not on the milestone list. Should comments be in scope for M1 or deferred to a post-M5 polish sprint?
→ Recommendation: Defer to post-M5.
Q7: Moderation
Shaders are auto-published for trusted submitters. Who reviews probation-tier submissions? Is there a mod queue UI needed, or is it email-based for now?
→ Recommendation: Simple admin endpoint (/api/v1/admin/queue) returning pending shaders + approve/reject actions. No admin UI needed for M4, just API.
End of Fractafrag Platform Specification v1.0
Built for GSD handoff — March 2026