Authentication & Setup
Basic GraphQL Client
const executeQuery = async (query, variables = {}) => {
const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'access-token': process.env.GALXE_ACCESS_TOKEN
},
body: JSON.stringify({ query, variables })
});
const data = await response.json();
if (data.errors) throw new Error(data.errors[0].message);
return data.data;
};
Error Handling with Retry
const retryQuery = async (query, variables, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await executeQuery(query, variables);
} catch (error) {
const isRetryable = error.message.includes('rate limit') ||
error.message.includes('timeout');
if (isRetryable && attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
continue;
}
throw error;
}
}
};
Quest Patterns
Quest Status Validation
query QuestStatus($id: ID!) {
quest(id: $id) {
status
startTime
endTime
}
}
const isQuestActive = (quest) => {
const now = Date.now() / 1000;
return quest.status === 'Active' &&
now >= quest.startTime &&
now <= quest.endTime;
};
const getQuestPhase = (quest) => {
const now = Date.now() / 1000;
if (now < quest.startTime) return 'upcoming';
if (now <= quest.endTime) return 'active';
return 'ended';
};
User Eligibility Check
query CheckEligibility($questId: ID!, $address: String!) {
quest(id: $questId) {
credentialGroups(address: $address) {
conditions { eligible }
rewards { eligible rewardType }
}
}
}
const checkEligibility = async (questId, address) => {
const data = await executeQuery(`
query CheckEligibility($questId: ID!, $address: String!) {
quest(id: $questId) {
credentialGroups(address: $address) {
conditions { eligible }
rewards { eligible }
}
}
}
`, { questId, address });
return data.quest.credentialGroups.some(group =>
group.conditions.some(c => c.eligible) &&
group.rewards.some(r => r.eligible)
);
};
Standard Pagination Query
query PaginatedQuery($first: Int!, $after: String) {
items(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
# your fields here
}
}
}
}
Fetch All Pages
const fetchAllPages = async (query, variables = {}, extractPath) => {
let allItems = [];
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const data = await executeQuery(query, {
...variables,
first: 100,
after: cursor
});
const result = extractPath(data);
allItems.push(...result.edges.map(edge => edge.node));
hasNextPage = result.pageInfo.hasNextPage;
cursor = result.pageInfo.endCursor;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
return allItems;
};
// Usage example
const allQuests = await fetchAllPages(
`query GetQuests($spaceId: ID!, $first: Int!, $after: String) {
quests(input: {spaceId: $spaceId, first: $first, after: $after}) {
pageInfo { hasNextPage endCursor }
list { id name status }
}
}`,
{ spaceId: "40" },
data => data.quests
);
Leaderboard Patterns
User Position Search
No Direct User Lookup: The B2B API does not provide direct user position lookup. You must search through leaderboard pages to find a specific user.
const findUserPosition = async (spaceId, userAddress) => {
let cursorAfter = null;
let pageCount = 0;
const maxPages = 20; // Reasonable search limit
while (pageCount < maxPages) {
const data = await executeQuery(`
query SearchUser($spaceId: Int!, $cursorAfter: String) {
space(id: $spaceId) {
loyaltyPointsRanks(cursorAfter: $cursorAfter) {
pageInfo { hasNextPage endCursor }
list {
rank
points
address { address username }
}
}
}
}
`, { spaceId, cursorAfter });
const userRank = data.space.loyaltyPointsRanks.list.find(item =>
item.address.address.toLowerCase() === userAddress.toLowerCase()
);
if (userRank) {
return { found: true, rank: userRank.rank, points: userRank.points };
}
if (!data.space.loyaltyPointsRanks.pageInfo.hasNextPage) break;
cursorAfter = data.space.loyaltyPointsRanks.pageInfo.endCursor;
pageCount++;
await new Promise(resolve => setTimeout(resolve, 100)); // Rate limiting
}
return { found: false, rank: null, points: 0 };
};
const getLeaderboardWithUser = async (spaceId, userAddress) => {
const [leaderboardData, userPosition] = await Promise.all([
executeQuery(`
query GetLeaderboard($spaceId: Int!) {
space(id: $spaceId) {
loyaltyPointsRanks(first: 10) {
list {
rank
points
address { username }
}
}
}
}
`, { spaceId }),
findUserPosition(spaceId, userAddress)
]);
return {
leaderboard: leaderboardData.space.loyaltyPointsRanks.list,
userPosition
};
};
Credential Patterns
Batch Eligibility Check
const checkBatchEligibility = async (credId, addresses) => {
const promises = addresses.map(address =>
executeQuery(`
query CheckEligibility($credId: ID!, $address: String!) {
credential(id: $credId) { eligible(address: $address) }
}
`, { credId, address })
.then(data => ({ address, eligible: data.credential.eligible === 1 }))
.catch(() => ({ address, eligible: false }))
);
return Promise.all(promises);
};
Credential Validation
const validateCredential = async (credId) => {
const data = await executeQuery(`
query ValidateCredential($credId: ID!) {
credential(id: $credId) {
syncStatus
itemCount
lastUpdate
}
}
`, { credId });
const cred = data.credential;
return {
isValid: cred.syncStatus === 'SYNCED',
hasHolders: cred.itemCount > 0,
lastSynced: new Date(cred.lastUpdate * 1000)
};
};
Real-time Monitoring
Simple Polling Monitor
const createMonitor = (queryFn, interval = 30000) => {
let intervalId = null;
const start = (callback) => {
intervalId = setInterval(async () => {
try {
const data = await queryFn();
callback({ type: 'update', data });
} catch (error) {
callback({ type: 'error', error: error.message });
}
}, interval);
};
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
};
return { start, stop };
};
// Usage
const leaderboardMonitor = createMonitor(
() => getLeaderboardWithUser(40, userAddress),
60000 // 1 minute
);
leaderboardMonitor.start((update) => {
if (update.type === 'update') {
console.log('Leaderboard updated:', update.data);
}
});
Rate Limiting
Simple Rate Limiter
class RateLimiter {
constructor(requestsPerSecond = 10) {
this.requests = [];
this.maxRequests = requestsPerSecond;
}
async execute(fn) {
const now = Date.now();
this.requests = this.requests.filter(time => now - time < 1000);
if (this.requests.length >= this.maxRequests) {
const waitTime = 1000 - (now - this.requests[0]);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
return fn();
}
}
// Usage
const limiter = new RateLimiter(5); // 5 requests per second
const data = await limiter.execute(() =>
executeQuery(query, variables)
);
Standardize User Objects
const standardizeUser = (userObject) => {
// Handle different user object formats
if (userObject.address) {
return {
id: userObject.address.id || userObject.address.address,
username: userObject.address.username,
address: userObject.address.address
};
}
return {
id: userObject.id,
username: userObject.username,
address: userObject.address
};
};
const formatRanking = (rankingData) => ({
rank: parseInt(rankingData.rank),
points: parseInt(rankingData.points),
user: standardizeUser(rankingData),
rankMovement: rankingData.delta || 0
});
const formatLeaderboard = (leaderboardData) => ({
totalCount: leaderboardData.totalCount,
rankings: leaderboardData.edges.map(edge => formatRanking(edge.node)),
hasNextPage: leaderboardData.pageInfo.hasNextPage
});
Utility Functions
Time Helpers
const timeHelpers = {
toUnix: (date) => Math.floor(date.getTime() / 1000),
fromUnix: (timestamp) => new Date(timestamp * 1000),
isAfter: (timestamp) => Date.now() / 1000 > timestamp,
formatDuration: (seconds) => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}d ${hours}h ${minutes}m`;
}
};
Validation Helpers
const validators = {
isValidAddress: (address) => /^0x[a-fA-F0-9]{40}$/.test(address),
isValidQuestId: (id) => typeof id === 'string' && id.length > 0,
isValidSpaceId: (id) => Number.isInteger(id) && id > 0
};
Best Practices Summary
- Always validate inputs before API calls
- Implement retry logic for network failures
- Use pagination for large datasets
- Cache frequently accessed data (30-60 seconds)
- Handle rate limits with exponential backoff
- Monitor for errors and log appropriately
- Use environment variables for sensitive data
Quick Reference
Essential Queries
- Quest status:
quest(id) { status startTime endTime }
- User eligibility:
quest(id) { credentialGroups(address) { conditions { eligible } } }
- Leaderboard:
space(id) { loyaltyPointsRanks(cursorAfter) { list { rank points address { username } } } }
- User search: Must paginate through leaderboard to find specific users
Common Response Patterns
- Paginated:
{ pageInfo { hasNextPage endCursor } edges { node { ... } } }
- User objects:
{ address { username address } }
or { username address }
- Error format:
{ errors [{ message extensions { code category } }] }
Next Steps