Leaderboard
Get user rankings based on activity in your Discord forum.
Get Leaderboard
GET /api/leaderboard/:serverIdParameters
| Parameter | Type | Description |
|---|---|---|
serverId | string | Discord server ID |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
type | enum | messages | messages, threads, reactions |
limit | number | 10 | Results (1-50) |
excludeBots | boolean | false | Exclude bot users |
botsOnly | boolean | false | Only bot users |
Response
{ "serverId": "123456789", "serverName": "My Server", "type": "messages", "filters": { "excludeBots": false, "botsOnly": false }, "leaderboard": [ { "rank": 1, "userId": "user001", "username": "super_helper", "globalName": "Super Helper", "avatar": "abc123", "isBot": false, "count": 523 }, { "rank": 2, "userId": "user002", "username": "active_dev", "globalName": "Active Developer", "avatar": "def456", "isBot": false, "count": 412 }, { "rank": 3, "userId": "bot001", "username": "ModBot", "globalName": null, "avatar": "ghi789", "isBot": true, "count": 350 } ], "cachedAt": "2024-01-15T12:00:00.000Z", "expiresAt": "2024-01-15T12:02:00.000Z"}Response Fields
| Field | Type | Description |
|---|---|---|
type | string | Ranking type |
filters | object | Applied filters |
leaderboard | array | Ranked users |
cachedAt | string | Cache timestamp |
expiresAt | string | Cache expiration |
Leaderboard Entry Fields
| Field | Type | Description |
|---|---|---|
rank | number | Position (1-based) |
userId | string | Discord user ID |
username | string | Username |
globalName | string | null | Display name |
avatar | string | null | Avatar hash |
isBot | boolean | Whether user is a bot |
count | number | Metric count |
Ranking Types
Messages
Total messages sent in forum threads:
curl "http://localhost:3000/api/leaderboard/123456789?type=messages"Threads
Total threads created:
curl "http://localhost:3000/api/leaderboard/123456789?type=threads"Reactions
Total reactions received on messages:
curl "http://localhost:3000/api/leaderboard/123456789?type=reactions"Filtering
Exclude Bots
curl "http://localhost:3000/api/leaderboard/123456789?excludeBots=true"Bots Only
curl "http://localhost:3000/api/leaderboard/123456789?botsOnly=true"Examples
curl "http://localhost:3000/api/leaderboard/123456789?type=messages&limit=10"curl "http://localhost:3000/api/leaderboard/123456789?type=messages&excludeBots=true&limit=20"curl "http://localhost:3000/api/leaderboard/123456789?type=threads&limit=10"curl "http://localhost:3000/api/leaderboard/123456789?type=reactions&limit=10"Implementation Examples
Leaderboard Component
function Leaderboard({ serverId }) { const [type, setType] = useState('messages'); const [excludeBots, setExcludeBots] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { async function fetchLeaderboard() { setLoading(true); const params = new URLSearchParams({ type, limit: '20', excludeBots: excludeBots.toString(), });
const response = await fetch(`/api/leaderboard/${serverId}?${params}`); const result = await response.json(); setData(result); setLoading(false); }
fetchLeaderboard(); }, [serverId, type, excludeBots]);
if (loading) return <div>Loading...</div>;
return ( <div className="leaderboard"> <div className="controls"> <select value={type} onChange={(e) => setType(e.target.value)}> <option value="messages">Messages</option> <option value="threads">Threads</option> <option value="reactions">Reactions</option> </select>
<label> <input type="checkbox" checked={excludeBots} onChange={(e) => setExcludeBots(e.target.checked)} /> Exclude bots </label> </div>
<ol className="rankings"> {data.leaderboard.map((entry) => ( <LeaderboardEntry key={entry.userId} entry={entry} type={type} /> ))} </ol>
<div className="cache-info"> Updated {formatRelative(data.cachedAt)} </div> </div> );}Leaderboard Entry
function LeaderboardEntry({ entry, type }) { const avatarUrl = entry.avatar ? `https://cdn.discordapp.com/avatars/${entry.userId}/${entry.avatar}.png?size=64` : `https://cdn.discordapp.com/embed/avatars/${(BigInt(entry.userId) >> 22n) % 6n}.png`;
const label = { messages: 'messages', threads: 'threads created', reactions: 'reactions received', }[type];
return ( <li className={`entry rank-${entry.rank}`}> <span className="rank">{entry.rank}</span>
<img className="avatar" src={avatarUrl} alt="" />
<div className="info"> <span className="name"> {entry.globalName || entry.username} {entry.isBot && <span className="bot-tag">BOT</span>} </span> <span className="username">@{entry.username}</span> </div>
<div className="count"> <span className="number">{entry.count.toLocaleString()}</span> <span className="label">{label}</span> </div> </li> );}Podium Display
function Podium({ leaderboard }) { const [second, first, third] = [ leaderboard[1], leaderboard[0], leaderboard[2], ];
return ( <div className="podium"> {second && <PodiumPlace entry={second} place={2} />} {first && <PodiumPlace entry={first} place={1} />} {third && <PodiumPlace entry={third} place={3} />} </div> );}
function PodiumPlace({ entry, place }) { const heights = { 1: 120, 2: 90, 3: 70 }; const medals = { 1: '🥇', 2: '🥈', 3: '🥉' };
return ( <div className={`podium-place place-${place}`}> <img className="avatar" src={getAvatarUrl(entry)} alt={entry.username} /> <span className="medal">{medals[place]}</span> <span className="name">{entry.globalName || entry.username}</span> <span className="count">{entry.count.toLocaleString()}</span> <div className="platform" style={{ height: heights[place] }} /> </div> );}CSS Styling
.leaderboard { max-width: 600px; margin: 0 auto;}
.leaderboard .controls { display: flex; gap: 16px; margin-bottom: 16px;}
.rankings { list-style: none; padding: 0; margin: 0;}
.entry { display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: #f5f5f5; margin-bottom: 8px;}
.entry.rank-1 { background: linear-gradient(90deg, #ffd700 0%, #f5f5f5 30%); }.entry.rank-2 { background: linear-gradient(90deg, #c0c0c0 0%, #f5f5f5 30%); }.entry.rank-3 { background: linear-gradient(90deg, #cd7f32 0%, #f5f5f5 30%); }
.entry .rank { font-weight: 700; font-size: 18px; width: 30px; text-align: center;}
.entry .avatar { width: 48px; height: 48px; border-radius: 50%;}
.entry .info { flex: 1;}
.entry .name { font-weight: 600; display: block;}
.entry .username { color: #666; font-size: 14px;}
.entry .count { text-align: right;}
.entry .count .number { font-weight: 700; font-size: 20px; display: block;}
.entry .count .label { font-size: 12px; color: #666;}
/* Podium */.podium { display: flex; justify-content: center; align-items: flex-end; gap: 16px; padding: 32px;}
.podium-place { display: flex; flex-direction: column; align-items: center;}
.podium-place .avatar { width: 64px; height: 64px; border-radius: 50%; border: 3px solid #5865f2;}
.podium-place .platform { width: 100px; background: linear-gradient(180deg, #5865f2, #4752c4); border-radius: 8px 8px 0 0; margin-top: 8px;}