Overview
Build loyalty programs that reward user engagement through:
- Quest-based point accumulation
- Leaderboard-driven competition
- User progress tracking via rankings
- Sprint-specific campaigns
Understanding Loyalty Points
Quest Points System
Each quest can award loyalty points to participants:
query GetQuestPoints($questId: ID!) {
quest(id: $questId) {
id
name
loyaltyPoints
status
space {
id
name
}
}
}
Response:
{
"data": {
"quest": {
"id": "GChdWUjXX3",
"name": "Introduction to Web3!",
"loyaltyPoints": 100,
"status": "Active",
"space": {
"id": "40",
"name": "BNB Chain"
}
}
}
}
Get User Points via Leaderboard
query GetLeaderboard($spaceId: Int!, $cursorAfter: String) {
space(id: $spaceId) {
id
name
loyaltyPointsRanks(cursorAfter: $cursorAfter) {
totalCount
pageInfo {
hasNextPage
endCursor
}
list {
rank
points
address {
username
avatar
address
}
}
}
}
}
Basic Implementation
Find User Points
async function findUserPoints(spaceId, userAddress, accessToken) {
let cursorAfter = null;
let pageCount = 0;
const maxPages = 20; // Reasonable search 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) {
totalCount
pageInfo { hasNextPage endCursor }
list {
rank
points
address { address username avatar }
}
}
}
}`,
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,
points: userRank.points,
rank: userRank.rank,
username: userRank.address.username,
totalParticipants: ranks.totalCount,
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,
points: 0,
rank: null,
totalParticipants: pageCount > 0 ? ranks.totalCount : 0,
searchPages: pageCount
};
}
async function getQuestPointValue(questId, 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 GetQuestPoints($questId: ID!) {
quest(id: $questId) {
id
name
loyaltyPoints
status
space { id name }
}
}`,
variables: { questId }
})
});
const data = await response.json();
return data.data.quest;
}
Sprint-Based Programs
Sprint vs Overall Comparison
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.
Compare User Progress
async function compareUserProgress(spaceId, userAddress, sprintId, accessToken) {
const [overall, sprint] = await Promise.all([
findUserPoints(spaceId, userAddress, accessToken),
findUserPointsInSprint(spaceId, userAddress, sprintId, accessToken)
]);
return {
overall: {
points: overall.points,
rank: overall.rank,
found: overall.found
},
sprint: {
points: sprint.points,
rank: sprint.rank,
found: sprint.found,
sprintId
},
comparison: {
pointsDifference: sprint.points - overall.points,
rankImprovement: overall.rank && sprint.rank
? overall.rank - sprint.rank
: null,
sprintPerformance: sprint.rank && overall.rank
? sprint.rank < overall.rank ? 'better' : 'worse'
: 'unknown'
}
};
}
// Similar to findUserPoints but with sprintId parameter
async function findUserPointsInSprint(spaceId, userAddress, sprintId, accessToken) {
// Implementation follows same pattern as findUserPoints
// but includes sprintId in the loyaltyPointsRanks query
}
User Dashboard Pattern
Create User Dashboard
async function createUserDashboard(spaceId, userAddress, accessToken) {
const userData = await findUserPoints(spaceId, userAddress, accessToken);
if (!userData.found) {
return {
user: {
address: userAddress,
points: 0,
rank: null,
isRanked: false,
message: 'User not found in top rankings'
},
leaderboard: {
totalParticipants: userData.totalParticipants,
searchedPages: userData.searchPages
}
};
}
// Calculate percentile
const percentile = userData.totalParticipants
? ((userData.totalParticipants - userData.rank) / userData.totalParticipants) * 100
: 0;
return {
user: {
address: userAddress,
username: userData.username,
points: userData.points,
rank: userData.rank,
isRanked: true,
percentile: Math.round(percentile)
},
leaderboard: {
totalParticipants: userData.totalParticipants,
searchedPages: userData.searchPages
},
spaceInfo: {
id: spaceId
}
};
}
Quest Integration
Track Quest Completion
async function trackQuestCompletion(questId, userAddress, accessToken) {
const quest = await getQuestPointValue(questId, accessToken);
if (!quest) {
return { error: 'Quest not found' };
}
if (quest.loyaltyPoints === 0) {
return { message: 'Quest does not award loyalty points' };
}
const userPoints = await findUserPoints(
parseInt(quest.space.id),
userAddress,
accessToken
);
return {
quest: {
id: quest.id,
name: quest.name,
points: quest.loyaltyPoints,
status: quest.status,
space: quest.space
},
user: {
currentPoints: userPoints.points,
currentRank: userPoints.rank,
projectedPoints: userPoints.points + quest.loyaltyPoints,
isCurrentlyRanked: userPoints.found
},
impact: {
pointsGain: quest.loyaltyPoints
}
};
}
Common Integration Patterns
Points-Based Access Control
async function checkPointsRequirement(spaceId, userAddress, requiredPoints, accessToken) {
const userPoints = await findUserPoints(spaceId, userAddress, accessToken);
return {
hasAccess: userPoints.points >= requiredPoints,
userPoints: userPoints.points,
requiredPoints,
deficit: Math.max(0, requiredPoints - userPoints.points),
rank: userPoints.rank,
isRanked: userPoints.found
};
}
Achievement Thresholds
function calculateAchievements(userPoints) {
const thresholds = [
{ name: 'Newcomer', points: 10 },
{ name: 'Explorer', points: 100 },
{ name: 'Adventurer', points: 500 },
{ name: 'Champion', points: 1000 },
{ name: 'Legend', points: 5000 }
];
const achieved = thresholds.filter(t => userPoints >= t.points);
const nextThreshold = thresholds.find(t => userPoints < t.points);
return {
current: achieved[achieved.length - 1] || { name: 'Beginner', points: 0 },
next: nextThreshold,
progress: nextThreshold
? (userPoints - achieved[achieved.length - 1]?.points || 0) / (nextThreshold.points - (achieved[achieved.length - 1]?.points || 0))
: 1,
totalAchieved: achieved.length
};
}
Usage Examples
Basic User Profile
// Get user loyalty program status
const accessToken = process.env.GALXE_ACCESS_TOKEN;
const spaceId = 40; // BNB Chain space
const userAddress = '0x1234567890123456789012345678901234567890';
const dashboard = await createUserDashboard(spaceId, userAddress, accessToken);
console.log('User Points:', dashboard.user.points);
console.log('User Rank:', dashboard.user.rank);
console.log('Percentile:', dashboard.user.percentile + '%');
console.log('Total Participants:', dashboard.leaderboard.totalParticipants);
Quest Rewards Preview
// Show potential rewards before quest participation
async function showQuestRewards(questId, userAddress, accessToken) {
const tracking = await trackQuestCompletion(questId, userAddress, accessToken);
if (tracking.error || tracking.message) {
console.log(tracking.error || tracking.message);
return;
}
console.log(`Quest: ${tracking.quest.name}`);
console.log(`Potential Points: +${tracking.quest.points}`);
console.log(`Current Points: ${tracking.user.currentPoints}`);
console.log(`After Completion: ${tracking.user.projectedPoints}`);
console.log(`Current Rank: ${tracking.user.currentRank || 'Unranked'}`);
}
Achievement System
// Check user achievements
const userPoints = await findUserPoints(spaceId, userAddress, accessToken);
const achievements = calculateAchievements(userPoints.points);
console.log(`Current Level: ${achievements.current.name}`);
if (achievements.next) {
console.log(`Next Level: ${achievements.next.name} (${achievements.next.points} points)`);
console.log(`Progress: ${Math.round(achievements.progress * 100)}%`);
}
console.log(`Total Achievements: ${achievements.totalAchieved}/5`);
Best Practices
- Search Optimization: Limit user search to 20-50 pages max for UI responsiveness
- Caching: Cache user positions for 5-10 minutes to reduce API calls
- Error Handling: Handle cases where users are not found in rankings
- Performance: Consider showing approximate positions rather than exact searches
- User Experience: Clearly indicate when users are not yet ranked
Limitations & Workarounds
Current Limitations
- No Direct User Lookup: Must search through leaderboard pages
- Search Performance: Finding users can require multiple API calls
- No Sprint Management: Sprint IDs come from external sources
Recommended Workarounds
- Efficient Search: Implement reasonable search limits
- Caching Strategy: Cache user positions for short periods
- User Communication: Set expectations about search time
- Achievement Systems: Define point thresholds instead of exact rankings
Points Integration Patterns
- New User Onboarding: Show potential points from available quests
- Progress Tracking: Regular user position checks (respect rate limits)
- Achievement Systems: Define point thresholds (100, 500, 1000, etc.)
- Access Control: Points-based feature unlocking
- Quest Recommendations: Suggest high-value quests based on current points
Next Steps