Use the eToro Users API to search for traders, pull performance data, and rank them in a real-time leaderboard.
Ranked lists of traders are a staple of social investing products: search, score, sort, refresh. This deep dive focuses on the Users API—how to discover accounts, pull performance and portfolio snapshots, enrich rows with Popular Investor (PI) metadata, and expose a sorted leaderboard that you can poll or cache behind your own API.
Base URL for all snippets: https://public-api.etoro.com/api/v1/. Include x-api-key on every request, x-user-key when the endpoint is user-scoped, and a fresh x-request-id (UUID) for observability.
Most leaderboards start with a search or discovery call rather than a hard-coded ID list. Pass a query string, optional filters (region, asset class, max risk), and paginate with page/pageSize or cursor fields as your portal documents.
import { randomUUID } from "node:crypto";
const BASE = "https://public-api.etoro.com/api/v1";
function usersHeaders() {
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 searchTraders({ query, page = 1, pageSize = 25 }) {
const qs = new URLSearchParams({
q: query,
page: String(page),
pageSize: String(pageSize),
});
const res = await fetch(`${BASE}/users/search?${qs}`, { headers: usersHeaders() });
if (!res.ok) throw new Error(`search failed: ${res.status}`);
const { data } = await res.json();
return data.items.map((u) => ({ id: u.userId, username: u.username }));
}
De-duplicate results by userId before you fan out into heavier endpoints.
For each candidate, request performance metrics for the windows you want to show (for example 12 months and all-time). Normalize percentages to numbers and keep raw payloads if you need to audit disputes later.
async function getPerformance(userId) {
const res = await fetch(
`${BASE}/users/${userId}/performance?windows=12m,all`,
{ headers: usersHeaders() }
);
if (!res.ok) throw new Error(`performance ${userId}: ${res.status}`);
const { data } = await res.json();
return {
return12m: data.windows["12m"].returnPercent,
returnAll: data.windows.all.returnPercent,
maxDrawdown: data.windows["12m"].maxDrawdownPercent,
riskScore: data.riskScore,
};
}
A leaderboard entry is more credible when you show diversification or top holdings, not just return. Call a portfolio or allocation endpoint to get weights by instrument or sector. Keep the call optional if your UI tolerates missing data when the market is closed.
async function getPortfolioSummary(userId) {
const res = await fetch(`${BASE}/users/${userId}/portfolio`, {
headers: usersHeaders(),
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`portfolio ${userId}: ${res.status}`);
const { data } = await res.json();
return {
topPositions: data.positions.slice(0, 5).map((p) => ({
symbol: p.symbol,
weight: p.weightPercent,
})),
};
}
Popular Investors often have badges, copier counts, and program tier fields. Fetch PI-specific metadata in one round trip if your API exposes something like /users/{id}/popular-investor, and merge it into the row for display chips (“PI”, tier, AUM cap).
async function getPopularInvestorProfile(userId) {
const res = await fetch(`${BASE}/pi-data/copiers/${userId}`, {
headers: usersHeaders(),
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`PI ${userId}: ${res.status}`);
const { data } = await res.json();
return {
tier: data.tier,
copiers: data.activeCopiers,
maxCopiers: data.maxCopiers,
badge: data.badges?.[0]?.label,
};
}
Pull search hits (or a static watchlist), enrich each user in parallel with bounded concurrency, compute a primary score (here: 12-month return), and sort. Expose lastUpdated so clients know how fresh the ranking is.
async function mapLimit(items, limit, fn) {
const out = [];
for (let i = 0; i < items.length; i += limit) {
const chunk = items.slice(i, i + limit);
out.push(...(await Promise.all(chunk.map(fn))));
}
return out;
}
export async function buildTraderLeaderboard(seedQuery) {
const traders = await searchTraders({ query: seedQuery, pageSize: 50 });
const rows = await mapLimit(traders, 5, async (t) => {
const [perf, pi, port] = await Promise.all([
getPerformance(t.id),
getPopularInvestorProfile(t.id),
getPortfolioSummary(t.id),
]);
return {
userId: t.id,
username: t.username,
score: perf.return12m,
metrics: perf,
pi,
portfolio: port,
};
});
rows.sort((a, b) => b.score - a.score);
return {
lastUpdated: new Date().toISOString(),
rows,
};
}
For a “live” feel, refresh the leaderboard on a cron (every few minutes) or when users open the screen, not on every keystroke. Store results in Redis or an edge cache and serve your UI from that layer. Log x-request-id from upstream errors so operations can trace spikes in 429 responses.
You now have an end-to-end Users API pipeline: search → score → enrich → rank → cache. Extend it with your own risk filters (for example exclude traders below a minimum copier count) before you display results to end users.
Was this helpful?
A step-by-step guide to building a simple trading bot using the eToro Demo Trading API.
How to programmatically search, filter, and explore eToro's instrument catalog — asset classes, exchanges, industries, and historical data.
Be the first to know when we publish new API guides, changelog entries, and developer resources.
Newsletter coming soon. We'll only email you when it launches.