Skip to main content
GET
https://api-mainnet.onzks.com
/
v1
/
scores
/
leaderboard
Get Leaderboard
curl --request GET \
  --url https://api-mainnet.onzks.com/v1/scores/leaderboard \
  --header 'Authorization: Bearer <token>'
{
  "success": true,
  "leaderboard": [
    {
      "rank": 123,
      "zksId": {},
      "address": "<string>",
      "score": 123,
      "percentile": 123,
      "tier": "<string>",
      "change": 123,
      "avatar": "<string>"
    }
  ],
  "pagination": {
    "total": 123,
    "limit": 123,
    "offset": 123,
    "hasMore": true
  }
}

Overview

Retrieve the global leaderboard showing top-ranked users by their ZKScore. This endpoint supports filtering by category, chain, and time period, making it ideal for displaying competitive rankings and discovering top performers.
Use this endpoint to build leaderboards, showcase top users, and create competitive features in your application.

Parameters

limit
number
Number of results to return (default: 50, max: 100)
offset
number
Number of results to skip for pagination (default: 0)
category
string
Filter by specific scoring category
  • activity - Transaction activity leaders
  • volume - Highest volume traders
  • age - Oldest wallets
  • diversity - Most diverse users
  • governance - Top DAO participants
  • social - Highest social reputation
  • risk - Best risk management
  • loyalty - Most loyal users
  • total - Overall score (default)
chainId
number
Filter by specific chain ID (optional, defaults to all chains)
timeframe
string
Time period for ranking
  • 24h - Last 24 hours
  • 7d - Last 7 days
  • 30d - Last 30 days (default)
  • all - All time

Response

success
boolean
Indicates if the request was successful
leaderboard
array
Array of top-ranked users
pagination
object

Examples

curl "https://api.onzks.com/v1/scores/leaderboard?limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response Example

{
  "success": true,
  "category": "total",
  "timeframe": "30d",
  "leaderboard": [
    {
      "rank": 1,
      "zksId": "alice",
      "address": "0x742d35cc6635c0532925a3b844d1ff4e1321",
      "score": 9847,
      "percentile": 99.9,
      "tier": "legendary",
      "change": 0,
      "avatar": "https://cdn.onzks.com/avatars/alice.png"
    },
    {
      "rank": 2,
      "zksId": "bob",
      "address": "0x1234567890123456789012345678901234567890",
      "score": 9756,
      "percentile": 99.8,
      "tier": "legendary",
      "change": 1,
      "avatar": null
    },
    {
      "rank": 3,
      "zksId": "charlie",
      "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
      "score": 9623,
      "percentile": 99.7,
      "tier": "legendary",
      "change": -1,
      "avatar": "https://cdn.onzks.com/avatars/charlie.png"
    }
    // ... more users
  ],
  "pagination": {
    "total": 125847,
    "limit": 50,
    "offset": 0,
    "hasMore": true
  }
}

Use Cases

1. Display Top Users

Show top 10 users in your app:
async function displayTopUsers() {
  const { leaderboard } = await getLeaderboard({ limit: 10 });

  const html = leaderboard.map((user, index) => `
    <div class="leaderboard-item">
      <span class="rank">#${user.rank}</span>
      <img src="${user.avatar || '/default-avatar.png'}" alt="${user.zksId}">
      <span class="name">${user.zksId || user.address.slice(0, 8)}</span>
      <span class="score">${user.score.toLocaleString()}</span>
      <span class="tier ${user.tier}">${user.tier}</span>
    </div>
  `).join('');

  document.getElementById('leaderboard').innerHTML = html;
}

2. Category-Specific Leaderboards

Show leaders in different categories:
async function displayCategoryLeaders() {
  const categories = ['volume', 'governance', 'social', 'loyalty'];
  
  for (const category of categories) {
    const { leaderboard } = await getLeaderboard({
      category,
      limit: 5
    });

    console.log(`\nTop ${category} leaders:`);
    leaderboard.forEach(user => {
      console.log(`  ${user.rank}. ${user.zksId} - ${user.score}`);
    });
  }
}

3. Paginated Leaderboard

Implement pagination for large leaderboards:
async function getPaginatedLeaderboard(page = 1, pageSize = 50) {
  const offset = (page - 1) * pageSize;
  
  const data = await getLeaderboard({
    limit: pageSize,
    offset
  });

  return {
    users: data.leaderboard,
    currentPage: page,
    totalPages: Math.ceil(data.pagination.total / pageSize),
    hasNext: data.pagination.hasMore,
    hasPrevious: page > 1
  };
}

// Usage
const page1 = await getPaginatedLeaderboard(1, 50);
const page2 = await getPaginatedLeaderboard(2, 50);

4. User Rank Lookup

Find a specific user’s position:
async function findUserRank(targetIdentity) {
  let offset = 0;
  const limit = 100;
  let found = false;

  while (!found) {
    const { leaderboard, pagination } = await getLeaderboard({
      limit,
      offset
    });

    const user = leaderboard.find(u => 
      u.zksId === targetIdentity || u.address === targetIdentity
    );

    if (user) {
      console.log(`${targetIdentity} is ranked #${user.rank}`);
      console.log(`Score: ${user.score}`);
      console.log(`Tier: ${user.tier}`);
      return user;
    }

    if (!pagination.hasMore) break;
    offset += limit;
  }

  console.log('User not found in leaderboard');
  return null;
}
Track users with biggest rank improvements:
async function getTrendingUsers() {
  const { leaderboard } = await getLeaderboard({
    limit: 100,
    timeframe: '7d'
  });

  // Sort by rank improvement
  const trending = leaderboard
    .filter(user => user.change > 0)
    .sort((a, b) => b.change - a.change)
    .slice(0, 10);

  console.log('Trending Users (Biggest Climbers):');
  trending.forEach(user => {
    console.log(`  ${user.zksId} - Up ${user.change} ranks to #${user.rank}`);
  });

  return trending;
}

Best Practices

1. Cache Leaderboard Data

Leaderboards don’t change frequently:
const leaderboardCache = new Map();

async function getCachedLeaderboard(options) {
  const cacheKey = JSON.stringify(options);
  
  if (leaderboardCache.has(cacheKey)) {
    const cached = leaderboardCache.get(cacheKey);
    // Cache for 5 minutes
    if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
      return cached.data;
    }
  }

  const data = await getLeaderboard(options);
  leaderboardCache.set(cacheKey, {
    data,
    timestamp: Date.now()
  });

  return data;
}

2. Implement Infinite Scroll

Load more users as user scrolls:
class LeaderboardScroller {
  constructor() {
    this.users = [];
    this.offset = 0;
    this.limit = 50;
    this.loading = false;
    this.hasMore = true;
  }

  async loadMore() {
    if (this.loading || !this.hasMore) return;

    this.loading = true;

    try {
      const { leaderboard, pagination } = await getLeaderboard({
        limit: this.limit,
        offset: this.offset
      });

      this.users.push(...leaderboard);
      this.offset += this.limit;
      this.hasMore = pagination.hasMore;

      return leaderboard;
    } finally {
      this.loading = false;
    }
  }

  reset() {
    this.users = [];
    this.offset = 0;
    this.hasMore = true;
  }
}

// Usage
const scroller = new LeaderboardScroller();
await scroller.loadMore(); // Load first page
await scroller.loadMore(); // Load second page

3. Highlight Current User

Show user’s position in leaderboard:
async function getLeaderboardWithUser(currentIdentity) {
  const { leaderboard } = await getLeaderboard({ limit: 50 });

  // Check if current user is in top 50
  const userIndex = leaderboard.findIndex(u => 
    u.zksId === currentIdentity || u.address === currentIdentity
  );

  if (userIndex >= 0) {
    return {
      leaderboard,
      currentUserIndex: userIndex,
      currentUserRank: leaderboard[userIndex].rank
    };
  }

  // If not in top 50, fetch user's actual rank
  const userScore = await getScore(currentIdentity);
  
  return {
    leaderboard,
    currentUserIndex: -1,
    currentUserRank: userScore.rank
  };
}

4. Real-time Updates

Poll for leaderboard updates:
class LiveLeaderboard {
  constructor(options = {}) {
    this.options = options;
    this.interval = null;
    this.updateCallback = null;
  }

  start(callback, pollInterval = 30000) {
    this.updateCallback = callback;
    
    // Initial load
    this.update();

    // Poll for updates
    this.interval = setInterval(() => {
      this.update();
    }, pollInterval);
  }

  async update() {
    try {
      const data = await getLeaderboard(this.options);
      if (this.updateCallback) {
        this.updateCallback(data);
      }
    } catch (error) {
      console.error('Failed to update leaderboard:', error);
    }
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }
}

// Usage
const liveBoard = new LiveLeaderboard({ limit: 10 });
liveBoard.start((data) => {
  console.log('Leaderboard updated:', data.leaderboard);
}, 30000); // Update every 30 seconds

Visualization Examples

Leaderboard Table

function renderLeaderboardTable(leaderboard) {
  return `
    <table class="leaderboard-table">
      <thead>
        <tr>
          <th>Rank</th>
          <th>User</th>
          <th>Score</th>
          <th>Tier</th>
          <th>Change</th>
        </tr>
      </thead>
      <tbody>
        ${leaderboard.map(user => `
          <tr class="${user.rank <= 3 ? 'top-three' : ''}">
            <td class="rank">#${user.rank}</td>
            <td class="user">
              <img src="${user.avatar || '/default.png'}" alt="${user.zksId}">
              <span>${user.zksId || user.address.slice(0, 8)}</span>
            </td>
            <td class="score">${user.score.toLocaleString()}</td>
            <td class="tier ${user.tier}">${user.tier}</td>
            <td class="change ${user.change > 0 ? 'up' : user.change < 0 ? 'down' : 'same'}">
              ${user.change > 0 ? '↑' : user.change < 0 ? '↓' : '→'} ${Math.abs(user.change)}
            </td>
          </tr>
        `).join('')}
      </tbody>
    </table>
  `;
}

Podium Display

function renderPodium(leaderboard) {
  const [first, second, third] = leaderboard.slice(0, 3);

  return `
    <div class="podium">
      <div class="position second">
        <div class="user">${second.zksId}</div>
        <div class="score">${second.score}</div>
        <div class="rank">2</div>
      </div>
      <div class="position first">
        <div class="user">${first.zksId}</div>
        <div class="score">${first.score}</div>
        <div class="rank">1</div>
      </div>
      <div class="position third">
        <div class="user">${third.zksId}</div>
        <div class="score">${third.score}</div>
        <div class="rank">3</div>
      </div>
    </div>
  `;
}

Troubleshooting

”Invalid category”

Cause: Unsupported category value. Solution:
  • Use supported categories: activity, volume, age, diversity, governance, social, risk, loyalty, total
  • Check for typos

”Limit exceeds maximum”

Cause: Requested limit is too high. Solution:
  • Maximum limit is 100
  • Use pagination for larger datasets
  • Request multiple pages if needed

”Leaderboard temporarily unavailable”

Cause: Leaderboard is being recalculated. Solution:
  • Wait a few minutes and try again
  • Leaderboards are recalculated periodically
  • Use cached data if available

Performance Tips

  1. Cache Results: Leaderboards change slowly, cache for 5+ minutes
  2. Use Appropriate Limits: Don’t request more data than needed
  3. Implement Pagination: Load data in chunks for better UX
  4. Debounce Updates: Don’t poll too frequently (30s minimum)

Rate Limits

Leaderboard requests are subject to rate limits:
  • Free tier: 30 requests per minute
  • Starter tier: 150 requests per minute
  • Professional tier: 600 requests per minute
  • Enterprise tier: Custom limits
Implement caching to stay within limits.