Overview
Build leaderboard systems that drive engagement through:
- Real-time user rankings via loyalty points
- Paginated leaderboard data with cursor-based pagination
- Sprint-specific leaderboards for competitions
- Space-level ranking analytics
Basic Leaderboard Query
Get Leaderboard Rankings
query GetLeaderboard($spaceId: Int!, $cursorAfter: String, $sprintId: Int) {
space(id: $spaceId) {
id
name
loyaltyPointsRanks(
cursorAfter: $cursorAfter
sprintId: $sprintId
) {
totalCount
pageInfo {
hasNextPage
endCursor
}
list {
rank
points
address {
username
avatar
address
}
}
}
}
}
Variables:
{
"spaceId": 40,
"cursorAfter": null,
"sprintId": null
}
Response:
{
"data": {
"space": {
"id": "40",
"name": "BNB Chain",
"loyaltyPointsRanks": {
"totalCount": 1500,
"pageInfo": {
"hasNextPage": true,
"endCursor": "cursor_end"
},
"list": [
{
"rank": 1,
"points": 5000,
"address": {
"username": "TopPlayer",
"avatar": "https://example.com/avatar.jpg",
"address": "0x1234567890123456789012345678901234567890"
}
}
]
}
}
}
}
Sprint-Specific Leaderboards
Get Sprint Rankings
When you have a sprint ID, get sprint-specific rankings:
query GetSprintLeaderboard($spaceId: Int!, $sprintId: Int!, $cursorAfter: String) {
space(id: $spaceId) {
loyaltyPointsRanks(
sprintId: $sprintId
cursorAfter: $cursorAfter
) {
totalCount
list {
rank
points
address {
username
address
}
}
}
}
}
Sprint Management: Sprint information must come from external sources as sprint management is not available through this API.
Cursor-Based Navigation
# Get first page
curl -X POST https://graphigo-business.prd.galaxy.eco/query \
-H "Content-Type: application/json" \
-H "access-token: YOUR_ACCESS_TOKEN" \
-d '{
"query": "query GetLeaderboard($spaceId: Int!) { space(id: $spaceId) { loyaltyPointsRanks(cursorAfter: null) { totalCount pageInfo { hasNextPage endCursor } list { rank points address { username } } } } }",
"variables": { "spaceId": 40 }
}'
Loading Next Page
- Use
pageInfo.endCursor
from previous response
- Set as
cursorAfter
parameter for next request
- Continue until
pageInfo.hasNextPage
is false
User Position Lookup
No Direct User Lookup: To find a specific user’s rank, you must paginate through the leaderboard.
Search Strategy
Since there’s no direct user lookup, implement efficient search:
- Limited Search: Search first 10-20 pages for UI responsiveness
- Background Search: Comprehensive search for analytics
- Caching: Store user positions to reduce future searches
Basic Implementation
async function findUserPosition(spaceId, userAddress, accessToken) {
let cursorAfter = null;
let pageCount = 0;
const maxPages = 20; // UI-friendly limit
while (pageCount < maxPages) {
const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'access-token': accessToken
},
body: JSON.stringify({
query: `query GetLeaderboard($spaceId: Int!, $cursorAfter: String) {
space(id: $spaceId) {
loyaltyPointsRanks(cursorAfter: $cursorAfter) {
pageInfo { hasNextPage endCursor }
list { rank points address { address username } }
}
}
}`,
variables: { spaceId, cursorAfter }
})
});
const data = await response.json();
const ranks = data.data.space.loyaltyPointsRanks;
// Search current page
const userRank = ranks.list.find(item =>
item.address.address.toLowerCase() === userAddress.toLowerCase()
);
if (userRank) {
return {
found: true,
rank: userRank.rank,
points: userRank.points,
username: userRank.address.username,
searchPages: pageCount + 1
};
}
if (!ranks.pageInfo.hasNextPage) break;
cursorAfter = ranks.pageInfo.endCursor;
pageCount++;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
return { found: false, searchPages: pageCount };
}
Common Integration Patterns
Top N Rankings
// Get top 10 users
async function getTopRankings(spaceId, count = 10, accessToken) {
const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'access-token': accessToken
},
body: JSON.stringify({
query: `query GetTopRankings($spaceId: Int!) {
space(id: $spaceId) {
name
loyaltyPointsRanks(cursorAfter: null) {
totalCount
list { rank points address { username avatar } }
}
}
}`,
variables: { spaceId }
})
});
const data = await response.json();
const leaderboard = data.data.space.loyaltyPointsRanks;
return {
spaceName: data.data.space.name,
totalParticipants: leaderboard.totalCount,
topUsers: leaderboard.list.slice(0, count)
};
}
Sprint vs Overall Comparison
async function compareRankings(spaceId, sprintId, accessToken) {
const [overall, sprint] = await Promise.all([
getTopRankings(spaceId, 10, accessToken),
getSprintRankings(spaceId, sprintId, 10, accessToken)
]);
return {
overall: overall.topUsers,
sprint: sprint.topUsers,
comparison: {
overallParticipants: overall.totalParticipants,
sprintParticipants: sprint.totalParticipants,
participationRate: sprint.totalParticipants / overall.totalParticipants
}
};
}
async function getSprintRankings(spaceId, sprintId, count, accessToken) {
// Similar to getTopRankings but with sprintId parameter
// Implementation details follow the same pattern
}
Best Practices
- Pagination: Use cursor-based pagination (
cursorAfter
/cursorBefore
)
- Rate Limiting: Add 200-500ms delays between pagination requests
- Caching: Cache leaderboard data for 30-60 seconds
- Search Limits: Limit user searches to reasonable page counts (20-50 pages)
- Error Handling: Handle cases where users are not found in rankings
- Sprint Context: Use
sprintId
for time-limited competitions
Current (Recommended)
cursorAfter
: Get items after this cursor
cursorBefore
: Get items before this cursor
sprintId
: Optional sprint ID for sprint-specific rankings
Deprecated (Avoid)
first
: Use cursor-based pagination instead
after
: Use cursor-based pagination instead
Limitations & Workarounds
Current Limitations
- No Direct User Lookup: Must search through pages
- No Sprint Management: Sprint IDs from external sources
- Search Performance: Finding users requires multiple API calls
Recommended Workarounds
- Efficient Search: Limit search scope for UI responsiveness
- Caching Strategy: Cache user positions for short periods
- User Communication: Set expectations about search time
- Fallback UI: Show leaderboard context even when user isn’t found
Usage Examples
Basic Leaderboard Display
// Get and display top 10 rankings
const topRankings = await getTopRankings(40, 10, accessToken);
console.log(`${topRankings.spaceName} Leaderboard`);
console.log(`Total participants: ${topRankings.totalParticipants}`);
console.log('\nTop 10:');
topRankings.topUsers.forEach(user => {
console.log(`${user.rank}. ${user.address.username} - ${user.points} points`);
});
User Position Check
// Find specific user's position
const userAddress = '0x1234567890123456789012345678901234567890';
const position = await findUserPosition(40, userAddress, accessToken);
if (position.found) {
console.log(`User rank: #${position.rank}`);
console.log(`User points: ${position.points}`);
console.log(`Search completed in ${position.searchPages} pages`);
} else {
console.log(`User not found in top ${position.searchPages * 50} participants`);
}
Next Steps