# Building a Trader Leaderboard with the Rankings API

Use the eToro Rankings API to filter investors, fetch period-scoped performance, and build a cache-friendly leaderboard.

---


Ranked trader lists no longer need a search call followed by one performance request per username. The dedicated **v2 Rankings API** returns identity, performance, risk, copier, and activity fields in one paginated response, with server-side filters and sorting.

All ranking calls use `https://public-api.etoro.com/api/v2/portfolios/rankings`. Send `x-api-key`, `x-user-key`, and a fresh `x-request-id` (UUID) with every request.

## Fetching a ranked page

The `period` query parameter is required. This example requests current-year Popular Investors, sorts by gain descending, and excludes risk scores above 6.

<!-- skip-test -->
```javascript
import { randomUUID } from "node:crypto";

const RANKINGS_API =
  "https://public-api.etoro.com/api/v2/portfolios/rankings";

function rankingsHeaders() {
  return {
    "x-api-key": process.env.ETORO_API_KEY,
    "x-user-key": process.env.ETORO_USER_KEY,
    "x-request-id": randomUUID(),
    accept: "application/json",
  };
}

async function getRankings({ page = 1, pageSize = 25 } = {}) {
  const query = new URLSearchParams({
    period: "CurrYear",
    sort: "-gain",
    page: String(page),
    pageSize: String(pageSize),
    popularInvestor: "true",
    riskScoreMax: "6",
  });

  const response = await fetch(`${RANKINGS_API}?${query}`, {
    headers: rankingsHeaders(),
  });
  if (!response.ok) {
    throw new Error(`Rankings failed: ${response.status}`);
  }

  return response.json();
}
```

The response contains `results` and `pagination`. A ranking row includes `username`, optional `fullName` and `avatarUrl`, `gain`, `riskScore`, `copiers`, `aumTier`, trading-activity metrics, and resolved sector or industry `tags`. The `gain` field is a decimal fraction, so `0.1234` means `12.34%`.

<!-- skip-test -->
```javascript
function toLeaderboardRows(response) {
  return response.results.map((investor, index) => ({
    rank: (response.pagination.page - 1) * response.pagination.pageSize + index + 1,
    username: investor.username,
    displayName: investor.fullName ?? investor.username,
    gainPercent: (investor.gain * 100).toFixed(2),
    riskScore: investor.riskScore,
    copiers: investor.copiers,
    aumTier: investor.aumTierDesc ?? investor.aumTier,
    tags: investor.tags ?? [],
  }));
}

const page = await getRankings({ pageSize: 20 });
console.table(toLeaderboardRows(page));
```

Use `pagination.hasNext` to decide whether to request another page. `totalItems` can be omitted on very large result sets, so do not rely on it to control pagination.

## Choosing filters and periods

Supported rolling periods include `CurrMonth`, `CurrQuarter`, `SixMonthsAgo`, `CurrYear`, `OneYearAgo`, `LastYear`, and `LastTwoYears`. Useful filters include:

- `country` using an ISO 3166-1 alpha-2 code
- `gainMin` and `gainMax`
- `copiersMin` and `copiersMax`
- `riskScoreMin` and `riskScoreMax`
- `popularInvestor`
- `aumTier`

Sort fields use camelCase; prefix the field with `-` for descending order, such as `-copiers` or `-gain`. Unknown sort values return HTTP 400.

## Ranking a known set of traders

When your product already has a curated username list, use the bulk endpoint instead of issuing one request per trader. It accepts up to 100 usernames, preserves their order, and omits missing, private, or unranked users.

<!-- skip-test -->
```javascript
async function getKnownTraders(usernames, period = "CurrYear") {
  const response = await fetch(
    `${RANKINGS_API}/multiple?period=${encodeURIComponent(period)}`,
    {
      method: "POST",
      headers: {
        ...rankingsHeaders(),
        "content-type": "application/json",
      },
      body: JSON.stringify({ usernames: usernames.slice(0, 100) }),
    }
  );

  if (!response.ok) {
    throw new Error(`Bulk rankings failed: ${response.status}`);
  }

  const { results } = await response.json();
  return results.map(({ username, value }) => ({
    username,
    gainPercent: (value.gain * 100).toFixed(2),
    riskScore: value.riskScore,
    copiers: value.copiers,
  }));
}
```

## Presets and discovery

`GET /api/v2/portfolios/rankings/presets` lists the available preset names. Apply one with `GET /api/v2/portfolios/rankings/presets/{type}`; examples include names such as `top-gainers` and `low-risk`. Use the list endpoint instead of hard-coding preset availability.

For custom discovery screens, `GET /api/v2/portfolios/rankings/tags` returns the industry and sector catalog, while `GET /api/v2/portfolios/rankings/summary?summary=industries` aggregates the ranking universe by a chosen dimension.

## Caching and responsible use

Rankings use the default shared quota, so requests to other endpoints without a dedicated limit draw from the same budget. Cache pages behind your own API, honor `RateLimit-*` and `Retry-After` response headers, and avoid refreshing on every keystroke.

Public ranking rows are privacy-aware: names and avatars may be absent, and private or unranked users may return 404 or be omitted from bulk results. Always fall back to `username`, tolerate missing optional fields, and do not present rankings as investment advice.
