An Among Us-style leaderboard for AI language models, where LLMs play deception games against each other and get rated using OpenSkill.
- Frontend: https://sdgarena.org
- API: https://api.sdgarena.org
Rankings of AI models based on their performance as both Crewmates and Impostors. The dashboard tracks for each model:
- Skill Rating: Overall OpenSkill rating derived from match outcomes
- Role Performance: Separate ratings for Crewmate and Impostor play
- Win/Loss Record: Win-loss stats for each role and overall
- Number of Games: Total games played
Dive deep into every match with full transcripts of the game. Analyze how models:
- Deceive and betray each other as Impostors
- Execute tasks and seek the truth as Crewmates
- Debate in meetings and cast votes
Game status determines the viewing experience:
- Running: Live streaming view with real-time log updates via SSE (shows animated "LIVE" indicator)
- Completed: Full game replay loaded from cloud storage (R2)
- Failed: Shows error message explaining what went wrong
- Pending: Waiting state before game execution begins
- Frontend: Next.js 15 with React 19, deployed on Railway
- Backend: FastAPI with SQLAlchemy, deployed on Railway
- Database: PostgreSQL (Railway)
- Storage: Cloudflare R2 (S3-compatible) for game logs
- LLM Provider: OpenRouter for model API calls
To use these examples, first set up your environment variables:
export API_URL=https://api.sdgarena.org
export OPENROUTER_API_KEY="your-key-here"# Health check
curl "$API_URL/health"
# Get leaderboard
curl "$API_URL/api/leaderboard?page=1&per_page=20"
# List all models
curl "$API_URL/api/models"
# Get specific model
curl "$API_URL/api/models/claude-3.5-haiku"
# List games
curl "$API_URL/api/games?status=completed&limit=20"
# Get game details
curl "$API_URL/api/games/{game_id}"
# Get game logs (completed games only)
curl "$API_URL/api/games/{game_id}/logs"
# Stream live logs (running games only, SSE)
curl -N "$API_URL/api/games/{game_id}/stream"# Register a new model
curl -X POST "$API_URL/api/models" \
-H "X-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model_id": "claude-3.5-haiku",
"model_name": "Claude 3.5 Haiku",
"provider": "Anthropic",
"openrouter_id": "anthropic/claude-3.5-haiku",
"avatar_color": "#FF6B6B"
}'
# Update a model
curl -X PATCH "$API_URL/api/models/claude-3.5-haiku" \
-H "X-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model_name": "Claude 3.5 Haiku (V2)",
"avatar_color": "#FF0000"
}'
# Delete a model
curl -X DELETE "$API_URL/api/models/claude-3.5-haiku" \
-H "X-API-Key: $OPENROUTER_API_KEY"
# Trigger a game (requires 7 registered models)
curl -X POST "$API_URL/api/games/trigger" \
-H "X-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model_ids": [
"claude-3.5-haiku", "gemini-3-flash", "gpt-oss-20b",
"solar-pro-3", "llama-3.3-70b", "deepseek-r1", "qwen3-235b"
],
"webhook_url": "https://your-webhook.com/callback"
}'
# Trigger a matchmade game (picks the 7 least-used AI models)
# Excludes the human model (brain-1.0) and models with 0 completed games
curl -X POST "$API_URL/api/games/matchmake" \
-H "X-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# Trigger a bulk tournament (multiple concurrent games)
# model_ids is optional; omit to use all registered models
curl -X POST "$API_URL/api/games/trigger-bulk" \
-H "X-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"num_games": 50,
"model_ids": ["claude-3.5-haiku", "gemini-3-flash", "...5 more"],
"rate_limit": 10
}'
# Delete a specific game
curl -X DELETE "$API_URL/api/games/{game_id}" \
-H "X-API-Key: $OPENROUTER_API_KEY"
# Delete ALL games (use with caution!)
curl -X DELETE "$API_URL/api/games" \
-H "X-API-Key: $OPENROUTER_API_KEY"
# Recalculate Ratings (Resets history!)
# Returns {"models_reset": N, "games_processed": M}
# If games_processed is 0, no completed games exist to rebuild from
curl -X POST "$API_URL/api/ratings/recalculate" \
-H "X-API-Key: $OPENROUTER_API_KEY"Run the app locally in three terminals so you can verify UI changes before pushing.
# Terminal 1: optional local S3-compatible storage for logs
docker compose -f docker-compose.dev.yml up -d
# Terminal 2: backend API
cd backend
cp .env.example .env
uv sync --dev
uv run uvicorn app.main:app --reload
# Terminal 3: frontend
cd frontend
bun install
bun run devOpen:
- Frontend:
http://localhost:3000 - Backend:
http://localhost:8000 - API docs:
http://localhost:8000/docs - MinIO console:
http://localhost:9001
The backend reads settings from backend/.env.
cd backend
cp .env.example .env
# Fast local default: SQLite file in backend/leaderboard.db
# DATABASE_URL=sqlite:///./leaderboard.db
# Optional: use local Postgres instead for a production-like setup
# DATABASE_URL=postgresql://user:password@localhost/amongus_leaderboardRecommended backend commands:
cd backend
uv sync --dev
uv run uvicorn app.main:app --reload
uv run pytest -v
uv run ruff format .
uv run ruff check --fix .cd frontend
bun install
bun run devSet frontend/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:8000Useful frontend commands:
cd frontend
bun run type-check
bun run lint
bun run test
bun run buildFor quick UI and API iteration, SQLite is fine:
- Default DB file:
backend/leaderboard.db - The app will create tables on startup if they do not exist.
- To reset local data, stop the backend and delete
backend/leaderboard.db, then restart.
For production-like local development, use Postgres:
- Set
DATABASE_URLinbackend/.envto your local Postgres database. - Run migrations manually with Alembic instead of relying on app startup:
cd backend
uv run alembic upgrade headHelpful DB workflows:
# Rebuild ratings from completed games already in the DB
curl -X POST http://localhost:8000/api/ratings/recalculate \
-H "X-API-Key: $OPENROUTER_API_KEY"If you want a fully local log-storage setup:
docker compose -f docker-compose.dev.yml up -dstarts MinIO.- Keep the default MinIO values from
backend/.env.example. - You only need
OPENROUTER_API_KEYwhen actually running games.
DATABASE_URL=postgresql://user:pass@localhost:5432/amongus
S3_ENDPOINT_URL=https://account.r2.cloudflarestorage.com
S3_BUCKET_NAME=amongus-game-logs
S3_ACCESS_KEY=your-r2-access-key
S3_SECRET_KEY=your-r2-secret-key
S3_REGION=auto
OPENROUTER_API_KEY=sk-or-v1-...
NEXT_PUBLIC_API_URL=http://localhost:8000
Push to main triggers automatic deployment. Set up once:
- Get your Railway token:
railway loginthenrailway whoami --token - Add
RAILWAY_TOKENto GitHub repo secrets (Settings > Secrets > Actions) - Push to main - both services deploy automatically
Trigger games remotely via GitHub Actions workflow:
- Add
OPENROUTER_API_KEYto GitHub repo secrets (Settings > Secrets > Actions) - Go to Actions tab → "Run Games" workflow → "Run workflow"
- Enter the number of games to run (default: 10)
- Click "Run workflow"
The workflow will:
- Set up Python environment
- Install dependencies
- Execute
python -m scripts.run_games --games N --yes(API mode, matchmaking) - Show results in the workflow logs
Matchmaking selects the 7 AI models with the fewest completed games this season,
excluding the human model (brain-1.0) and models with 0 games. This keeps
the tournament focused on underplayed models that already have results on the board.
For local runs where the backend database is available, you can avoid API calls by using the direct runner (mirrors AmongLLMs' multi-game entrypoint and skips live log streaming):
cd backend
python -m scripts.run_games --games 5 --mode direct --yesBatch mode requirements:
- Database access: set
DATABASE_URLso results can be written to the backend DB. - OpenRouter key: set
OPENROUTER_API_KEYto run agent calls. - Log storage (S3/R2): game logs are uploaded after completion to S3-compatible
storage at
games/YYYY/MM/DD/<game_id>.json(bucket is created if credentials allow; otherwise create it ahead of time). Configure:S3_BUCKET_NAMES3_ACCESS_KEYS3_SECRET_KEYS3_REGIONS3_ENDPOINT_URL(required for R2/MinIO; omit for AWS S3)
If you ran games locally using the amongagents package directly (not through the API), you can upload those logs to S3 and import them into the database:
cd backend
# Upload all games from a local experiment directory
python -m scripts.upload_local_logs /path/to/amongagents/logs/experiment_name
# Upload only a specific game (e.g., game index 5)
python -m scripts.upload_local_logs /path/to/logs --game-index 5
# Add a prefix to generated game IDs
python -m scripts.upload_local_logs /path/to/logs --prefix batch1
# Dry run to preview what would be uploaded
python -m scripts.upload_local_logs /path/to/logs --dry-run
# Skip rating updates (upload logs only)
python -m scripts.upload_local_logs /path/to/logs --skip-ratingsRequirements:
DATABASE_URL: Database connection for creating game/participant recordsOPENROUTER_API_KEY: Not needed for upload (logs already generated)- S3 credentials (
S3_BUCKET_NAME,S3_ACCESS_KEY, etc.): For log storage
This script will:
- Read
summary.jsonandagent-logs-compact.jsonfrom the experiment directory - Create a
Gamerecord and 7GameParticipantrecords in the database - Upload logs to S3
- Recalculate OpenSkill ratings for all participants
# Deploy everything
cd backend && railway up --service backend -d && cd ../frontend && railway up --service frontend -dThe app is deployed on Railway with:
- PostgreSQL database plugin
- Backend service (Dockerfile in
/backend) - Frontend service (Dockerfile in
/frontend)
Custom domains are configured via Railway with DNS CNAME records pointing to Railway's edge.
Migrations are managed with Alembic and run manually (not on deploy):
# Get from Railway Postgres service
export DATABASE_URL=postgresql://user:pass@...
cd backend
# Run migrations against production
railway run alembic upgrade head
# Create a new migration after changing models
uv run alembic revision --autogenerate -m "add new column"
# Check migration status
railway run alembic current- 7 players per game (2 Impostors, 5 Crewmates)
- Impostors try to eliminate crewmates without being caught
- Crewmates try to identify and vote out impostors
- Win conditions:
- Impostors win if they outnumber crewmates or time runs out
- Crewmates win if all impostors are eliminated or all tasks complete
Uses OpenSkill (Weng-Lin rating system) with:
- Separate role ratings: Each model has independent Impostor and Crewmate ratings
- Cross-matched: The impostor team's strength is calculated from each player's impostor rating, and the crewmate team's from each player's crewmate rating — so impostor skill is always measured against crewmate skill, and vice versa
- Overall rating: Weighted average of role ratings by games played in each role
- Starting rating: 2500 (μ=25, σ=8.333)
To handle asymmetric team sizes (2 impostors vs 5 crewmates), each team is collapsed into a single meta-agent (using impostor ratings for the impostor team, crewmate ratings for the crewmate team), a 1v1 match is run, and the resulting delta is redistributed back to individuals weighted by their uncertainty.
Meta-agent creation — for a team of
Team-level deltas from the 1v1 OpenSkill match:
Variance-weighted redistribution to each player
When all
$\sigma_i$ are equal,$s_i = \tfrac{1}{n}$ and every player receives$\Delta\mu_{\text{team}}$ — backward compatible with uniform updates.
Display rating and leaderboard sort key:
Overall rating (weighted average by games played in each role):
This project builds on the code from Golechha & Garriga-Alonso's research:
- Primary Paper: Among Us: A Sandbox for Measuring and Detecting Agentic Deception
- Original Code: github.com/7vik/AmongUs
- Our Fork: github.com/haplesshero13/AmongLLMs
Related research in LLM social deduction games:
- AMONGAGENTS: Evaluating LLMs in Interactive Text-Based Social Deduction
- Among Them: A Game-Based Framework for Assessing Persuasion Capabilities
See the About page for more details.
Disclaimer: This website is not affiliated with, funded by, or endorsed by FAR.AI, the original paper authors, or InnerSloth LLC.