# eToro Builders Portal — Full Documentation > Build on the world's leading social trading platform. APIs, Agent Skills, MCP, and tools to power personal workflows, production apps, and platform integrations. > Source: https://builders.etoro.com ## Getting Started & Reference ### Getting Started URL: https://builders.etoro.com/get-started Description: Step-by-step onboarding — create an account, get API keys, and make your first call in under 5 minutes. ### API Docs (external) URL: https://api-portal.etoro.com Description: Full API reference, endpoint schemas, and integration guides on the eToro API Portal. ### Changelog (external) URL: https://api-portal.etoro.com/changelog Description: API releases, improvements, and breaking changes on the eToro API Portal. ### Playground URL: https://builders.etoro.com/playground Description: Interactive API experimentation — pick endpoints, send requests, and inspect live responses. ### WebSocket Playground URL: https://builders.etoro.com/playground/websocket Description: Real-time WebSocket testing — subscribe to streams, inspect events, and debug connections. ### API Reference URL: https://builders.etoro.com/reference Description: Full endpoint reference, request/response schemas, and authentication details. ### App Registration URL: https://builders.etoro.com/app-registration Description: Register your integration for SSO, partner access, and production API keys. ### App Store URL: https://builders.etoro.com/appstore Description: How the eToro app App Store works and how builders can prepare for it. ### FAQ URL: https://builders.etoro.com/faq Description: Common questions, troubleshooting, and support entry points. ### Glossary URL: https://builders.etoro.com/glossary Description: Definitions for eToro platform terms, API concepts, and builder vocabulary. ### Examples URL: https://builders.etoro.com/examples Description: Code examples and sample projects for common build patterns. ### Status URL: https://builders.etoro.com/status Description: Real-time platform health and API uptime monitoring. ## Products ### Trading URL: https://builders.etoro.com/products/trading Description: Execute and manage trades — real and demo ### Market Data & Real-Time URL: https://builders.etoro.com/products/market-data-realtime Description: REST snapshots plus live streaming ### Portfolio & Account URL: https://builders.etoro.com/products/portfolio-account Description: Who is signed in and which account context applies ### Watchlists URL: https://builders.etoro.com/products/watchlists Description: Create and manage watchlists ### Social & Discovery URL: https://builders.etoro.com/products/social-discovery Description: Feeds, profiles, comments, and Pro Investors ### Agent Portfolios URL: https://builders.etoro.com/products/agent-portfolios Description: AI-managed portfolios with token delegation ## Use Cases ### Personal Use URL: https://builders.etoro.com/use-cases/personal-use Description: Self-serve API access for your own investing workflows—dashboards, alerts, bots, and research—using API keys and the same REST surfaces as production integrations. ### Build eToro Apps URL: https://builders.etoro.com/use-cases/build-apps Description: Design and ship new experiences—mobile and web apps, analytics products, AI copilots, and app-store-ready builds—on top of eToro APIs, examples, and agent tooling. ### Partners & Integrations URL: https://builders.etoro.com/use-cases/partners-integrations Description: Register applications, connect identity and SSO-style flows, and embed eToro market, portfolio, and trading capabilities inside your existing product stack. ### Algorithmic Trading URL: https://builders.etoro.com/use-cases/algo-trading Description: Automate strategies. Backtest. Deploy. ### Portfolio Apps URL: https://builders.etoro.com/use-cases/portfolio-apps Description: Build analytics, trackers, and dashboards. ### Social Trading Analytics URL: https://builders.etoro.com/use-cases/social-analytics Description: Tap into eToro's social graph. Unique data. ### Fintech Integrations URL: https://builders.etoro.com/use-cases/fintech-integrations Description: Embed market data into your product. ### AI Agents & Copilots URL: https://builders.etoro.com/use-cases/ai-agents Description: MCP server. Connect your AI code editor. ## Tools ### eToro Agent Skills URL: https://builders.etoro.com/tools/skills Description: A SKILL.md file that teaches any AI agent how to authenticate, trade, and query the eToro API — works in Cursor, Claude Code, Codex, Antigravity, and 30+ other tools. ### MCP Server URL: https://builders.etoro.com/tools/mcp Description: Model Context Protocol server that connects any AI IDE to eToro API documentation, schemas, and endpoint references. ### Cursor URL: https://builders.etoro.com/tools/cursor Description: AI-native code editor with built-in MCP support. Add the eToro MCP server and start building with inline API context. ### Claude Code URL: https://builders.etoro.com/tools/claude-code Description: Anthropic's terminal-based coding agent with MCP support. Connect it to eToro for agentic development from the command line. ### Antigravity URL: https://builders.etoro.com/tools/antigravity Description: Google's agent-first IDE designed for autonomous execution and vibe coding. Configure the eToro MCP server to let its agents access API references. ### Base44 URL: https://builders.etoro.com/tools/base44 Description: AI-powered app builder. Describe what you want, connect the eToro API, and get a working app. ### Lovable URL: https://builders.etoro.com/tools/lovable Description: AI app creation platform. Describe your product in natural language, then wire it to eToro APIs. ### eToro CLI URL: https://builders.etoro.com/tools/etoro-cli Description: Trade, invest, and copy from your terminal. Beautiful colored tables by default, `--output json` everywhere for scripts and AI agents. ### API Playground URL: https://builders.etoro.com/tools/playground Description: Interactive API explorer in the browser. Pick an endpoint, fill in parameters, send requests, and inspect live responses. ## Tutorials & Guides ### Portfolio Management & Position Tracking URL: https://builders.etoro.com/learn/portfolio-management-and-positions Description: Track positions, calculate P&L, monitor account balances, and manage your portfolio programmatically through the eToro API. ## Overview The Portfolio API gives you programmatic access to your trading positions, account balance, and profit/loss data. This guide covers how to fetch portfolio data, track individual positions, calculate returns, and build a portfolio monitoring system. > **Prerequisite:** You should be comfortable with [API authentication](/learn/authentication-and-api-keys) before working with portfolio endpoints. ## Portfolio API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/trading/info/portfolio` | GET | Full portfolio snapshot with all positions | | `/trading/info/real/pnl` | GET | P&L, credits, and account balances | | `/trading/info/trade/history` | GET | Closed position history | | `/api/v2/trading/positions/{positionId}` | PATCH | Update stop-loss or take-profit on a live position | | `/api/v2/trading/demo/positions/{positionId}` | PATCH | Update stop-loss or take-profit on a demo position | ## Fetching Your Portfolio Get a complete snapshot of all open positions: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getPortfolio() { const response = await fetch(`${API_BASE}/trading/info/portfolio`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); if (!response.ok) { throw new Error(`Portfolio fetch failed: ${response.status}`); } return response.json(); } const portfolio = await getPortfolio(); console.log(`Open positions: ${portfolio.positions.length}`); console.log(`Total equity: $${portfolio.equity.toFixed(2)}`); ``` ## Position Details Each position in the portfolio includes key trading data: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getPositions() { const response = await fetch(`${API_BASE}/trading/info/portfolio`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); const positions = await response.json(); return positions.map((p) => ({ id: p.positionId, instrument: p.instrumentName, direction: p.isBuy ? "LONG" : "SHORT", openPrice: p.openRate, currentPrice: p.currentRate, amount: p.investedAmount, pnl: p.netProfit, pnlPercent: ((p.netProfit / p.investedAmount) * 100).toFixed(2), stopLoss: p.stopLossRate, takeProfit: p.takeProfitRate, openDate: new Date(p.openDateTime).toLocaleDateString(), })); } const positions = await getPositions(); positions.forEach((p) => { const emoji = p.pnl >= 0 ? "+" : ""; console.log( `${p.instrument} ${p.direction} | ${emoji}$${p.pnl.toFixed(2)} (${emoji}${p.pnlPercent}%)` ); }); ``` ## Updating Stop-Loss and Take-Profit Open positions can be updated without closing and reopening them. Use the live or demo v2 position endpoint and provide at least one stop-loss or take-profit field: ```javascript skip-test import { randomUUID } from "node:crypto"; async function updatePositionRisk( positionId, changes, environment = "demo" ) { const url = environment === "demo" ? `https://public-api.etoro.com/api/v2/trading/demo/positions/${positionId}` : `https://public-api.etoro.com/api/v2/trading/positions/${positionId}`; const response = await fetch(url, { method: "PATCH", headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), "content-type": "application/json", }, body: JSON.stringify(changes), }); if (response.status !== 202) { throw new Error(`Position update failed: ${response.status}`); } return response.json(); } const accepted = await updatePositionRisk(13902598, { stopLossRate: 145.25, takeProfitRate: 165.5, stopLossType: "fixed", }); console.log("Accepted operation:", accepted.operationId); ``` The API returns HTTP 202 because the edit is processed asynchronously. Keep the returned `operationId`, `positionId`, and `referenceId` for reconciliation. To remove protection instead of changing its rate, send `clearStopLoss: true` or `clearTakeProfit: true`. ## Account Balance and Equity Monitor your account health in real time: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getAccountBalance() { const response = await fetch(`${API_BASE}/trading/info/real/pnl`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); const balance = await response.json(); return { totalBalance: balance.totalBalance, availableBalance: balance.availableBalance, equity: balance.equity, unrealizedPnL: balance.equity - balance.totalBalance, marginUsed: balance.totalBalance - balance.availableBalance, marginLevel: balance.totalBalance > 0 ? ((balance.equity / balance.totalBalance) * 100).toFixed(1) : "N/A", }; } const account = await getAccountBalance(); console.log(`Equity: $${account.equity.toFixed(2)}`); console.log(`Available: $${account.availableBalance.toFixed(2)}`); console.log(`Unrealized P&L: $${account.unrealizedPnL.toFixed(2)}`); ``` ## Calculating Portfolio Returns Build a function that aggregates position data into portfolio-level metrics: ```javascript skip-test function calculatePortfolioMetrics(positions) { const totalInvested = positions.reduce( (sum, p) => sum + p.amount, 0 ); const totalPnL = positions.reduce((sum, p) => sum + p.pnl, 0); const winners = positions.filter((p) => p.pnl > 0); const losers = positions.filter((p) => p.pnl < 0); return { totalPositions: positions.length, totalInvested: totalInvested.toFixed(2), totalPnL: totalPnL.toFixed(2), returnPercent: ((totalPnL / totalInvested) * 100).toFixed(2), winRate: ((winners.length / positions.length) * 100).toFixed(1), winners: winners.length, losers: losers.length, bestPosition: positions.reduce( (best, p) => (p.pnl > (best?.pnl || -Infinity) ? p : best), null ), worstPosition: positions.reduce( (worst, p) => (p.pnl < (worst?.pnl || Infinity) ? p : worst), null ), }; } ``` ## Trade History Fetch closed positions to analyze historical performance: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getTradeHistory(options = {}) { const params = new URLSearchParams({ limit: options.limit || 50, ...(options.startDate && { startDate: options.startDate }), ...(options.endDate && { endDate: options.endDate }), }); const response = await fetch( `${API_BASE}/trading/info/trade/history?${params}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, } ); const history = await response.json(); return history.map((trade) => ({ instrument: trade.instrumentName, direction: trade.isBuy ? "LONG" : "SHORT", openDate: trade.openDateTime, closeDate: trade.closeDateTime, openPrice: trade.openRate, closePrice: trade.closeRate, invested: trade.investedAmount, pnl: trade.netProfit, holdingDays: Math.ceil( (new Date(trade.closeDateTime) - new Date(trade.openDateTime)) / (1000 * 60 * 60 * 24) ), })); } // Get last 30 days of trades const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) .toISOString() .split("T")[0]; const history = await getTradeHistory({ startDate: thirtyDaysAgo }); console.log(`Closed ${history.length} trades in the last 30 days`); ``` ## Building a Portfolio Monitor Combine the pieces into a real-time portfolio monitoring function: ```javascript skip-test async function monitorPortfolio(intervalMs = 60000) { console.log("Starting portfolio monitor...\n"); async function check() { const [positions, balance] = await Promise.all([ getPositions(), getAccountBalance(), ]); const metrics = calculatePortfolioMetrics(positions); console.clear(); console.log("=== Portfolio Monitor ==="); console.log(`Time: ${new Date().toLocaleTimeString()}`); console.log(`Equity: $${balance.equity.toFixed(2)}`); console.log(`Available: $${balance.availableBalance.toFixed(2)}`); console.log(`Positions: ${metrics.totalPositions}`); console.log(`P&L: $${metrics.totalPnL} (${metrics.returnPercent}%)`); console.log(`Win rate: ${metrics.winRate}%`); console.log(); // Alert on significant drawdown if (parseFloat(metrics.returnPercent) < -5) { console.warn("WARNING: Portfolio drawdown exceeds 5%"); } // Log position details positions.forEach((p) => { const sign = p.pnl >= 0 ? "+" : ""; console.log( ` ${p.instrument.padEnd(12)} ${p.direction.padEnd(6)} ${sign}$${p.pnl.toFixed(2).padStart(10)}` ); }); } await check(); setInterval(check, intervalMs); } // Monitor every 60 seconds monitorPortfolio(60000); ``` ## Demo vs Real Portfolio The same code works for both demo and real accounts — the only difference is the endpoint path: | Environment | Portfolio Endpoint | |-------------|-------------------| | Demo | `/trading/info/demo/portfolio` | | Real | `/trading/info/portfolio` | ```javascript skip-test function getPortfolioPath(environment = "demo") { return environment === "demo" ? "trading/info/demo/portfolio" : "trading/info/portfolio"; } const API_BASE = "https://public-api.etoro.com/api/v1"; const portfolioPath = getPortfolioPath(process.env.ETORO_ENVIRONMENT); const portfolioUrl = `${API_BASE}/${portfolioPath}`; ``` > **Tip:** Always develop and test with demo portfolios first. Switch to real only after thorough testing. ## Best Practices 1. **Poll wisely** — Don't fetch portfolio data more than once per minute. Position data doesn't change that frequently unless you're actively trading. 2. **Use WebSocket for real-time** — For live price updates on open positions, connect to the [WebSocket API](/learn/real-time-market-data-websocket) instead of polling REST. 3. **Handle empty portfolios** — New accounts or accounts with no open positions return empty arrays. Handle this gracefully. 4. **Store historical snapshots** — Record portfolio snapshots periodically for performance tracking and debugging. 5. **Mind your rate limit** — All requests count toward your rate limit. See the [Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits) for details. ## Next Steps - [Building an Algo Trading Bot](/learn/building-an-algo-trading-bot) — Automate your trading strategy - [Authentication Deep Dive](/learn/authentication-and-api-keys) — Secure your API credentials - [Real-Time Market Data](/learn/real-time-market-data-websocket) — Stream live prices - [API Reference](https://api-portal.etoro.com/api-reference) — Full endpoint documentation --- ### Social Trading Data & Copy Trading URL: https://builders.etoro.com/learn/social-trading-and-copy-trading Description: Access eToro's social layer programmatically — user profiles, social feeds, Pro Investor stats, and copy trading data through the API. ## Overview eToro's social trading platform generates rich data — user profiles, trade activity feeds, Pro Investor rankings, and copy trading relationships. The Social Feeds and Users Info APIs let you tap into this data programmatically to build analytics dashboards, leaderboards, and automated social strategies. ## Social Trading API Surfaces | API Surface | What It Provides | Key Endpoints | |-------------|-----------------|---------------| | **Social Feeds** | Activity feed of trades, posts, and comments | `/feeds/markets/{marketId}`, `/feeds/users/{userId}` | | **Users Info** | Public profile data and trading stats | `/user-info/people` | | **Rankings** | Period-scoped investor rankings, filters, presets, and summaries | `/api/v2/portfolios/rankings` | | **Comments** | Discussion threads on posts | `/posts/{postId}/comments` | ## Fetching User Profiles Retrieve public profile data including trading statistics, risk score, and performance history: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getUserProfile(username) { const response = await fetch(`${API_BASE}/user-info/people?usernames=${username}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); const profile = await response.json(); return { displayName: profile.displayName, riskScore: profile.riskScore, copiers: profile.copiers, totalTrades: profile.totalTrades, profitableWeeks: profile.profitableWeeksPct, verified: profile.isVerified, }; } const trader = await getUserProfile("pro_investor_123"); console.log(`${trader.displayName} — Risk: ${trader.riskScore}, Copiers: ${trader.copiers}`); ``` ## Reading the Social Feed The social feed streams trade activity, user posts, and market commentary: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getSocialFeed(options = {}) { const instrumentId = options.instrumentId; if (!instrumentId) { throw new Error("instrumentId is required for GET /feeds/markets/{marketId}"); } const params = new URLSearchParams({ take: String(options.limit || 20), ...(options.type && { type: options.type }), }); const response = await fetch( `${API_BASE}/feeds/markets/${instrumentId}?${params}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); return response.json(); } // Get recent feed items for an instrument (resolve TSLA to instrumentId via /market-data/search first) const teslaPosts = await getSocialFeed({ instrumentId: 123456, limit: 10, }); teslaPosts.items.forEach((item) => { console.log(`[${item.type}] ${item.user.displayName}: ${item.text?.slice(0, 80)}...`); }); ``` ### Feed Item Types | Type | Description | |------|-------------| | `trade_open` | User opened a new position | | `trade_close` | User closed a position (includes P&L) | | `post` | User published a text post | | `comment` | User commented on an activity | ## Building a Trader Leaderboard Use the dedicated v2 Rankings API to filter and sort investors without making one profile request per row. The `period` parameter is required, and gain values are returned as decimal fractions (`0.12` means `12%`): ```javascript skip-test import { randomUUID } from "node:crypto"; const RANKINGS_API = "https://public-api.etoro.com/api/v2/portfolios/rankings"; async function getTopTraders(count = 10) { const params = new URLSearchParams({ period: "CurrYear", sort: "-gain", page: "1", pageSize: String(count), popularInvestor: "true", }); const response = await fetch(`${RANKINGS_API}?${params}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); if (!response.ok) { throw new Error(`Rankings failed: ${response.status}`); } const { results } = await response.json(); return results.map((t, i) => ({ rank: i + 1, name: t.fullName ?? t.username, gainPercent: (t.gain * 100).toFixed(2), riskScore: t.riskScore, copiers: t.copiers, })); } const leaderboard = await getTopTraders(5); console.table(leaderboard); ``` ## Working with Comments Comments live on feed posts. Read them from the post returned by the feed, then add your own with `POST /posts/{postId}/comments` (the post ID goes in the URL): ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; // Read the comments attached to a feed post (they come back on the post itself) async function getComments(postId) { const response = await fetch(`${API_BASE}/posts/${postId}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); const post = await response.json(); return (post.comments ?? []).map((c) => ({ author: c.user.displayName, text: c.body, likes: c.likes, timestamp: new Date(c.createdAt).toLocaleString(), })); } // Add a comment to a feed post async function postComment(postId, message) { const response = await fetch(`${API_BASE}/posts/${postId}/comments`, { method: "POST", headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ message }), }); return response.json(); } ``` ## Sentiment Analysis Use Case Use social feed data to gauge market sentiment for an instrument: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; async function getSocialFeed(options = {}) { const instrumentId = options.instrumentId; if (!instrumentId) { throw new Error("instrumentId is required for GET /feeds/markets/{marketId}"); } const params = new URLSearchParams({ take: String(options.limit || 20), ...(options.type && { type: options.type }), }); const response = await fetch( `${API_BASE}/feeds/markets/${instrumentId}?${params}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, } ); return response.json(); } async function analyzeSentiment(instrumentId, hours = 24) { const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); const feed = await getSocialFeed({ instrumentId, limit: 100, }); const trades = feed.items.filter( (item) => item.type === "trade_open" && item.createdAt > since ); const buys = trades.filter((t) => t.direction === "BUY").length; const sells = trades.filter((t) => t.direction === "SELL").length; const total = buys + sells; return { instrumentId, period: `${hours}h`, totalTrades: total, buyPercentage: total > 0 ? ((buys / total) * 100).toFixed(1) : 0, sellPercentage: total > 0 ? ((sells / total) * 100).toFixed(1) : 0, sentiment: buys > sells ? "bullish" : sells > buys ? "bearish" : "neutral", }; } const sentiment = await analyzeSentiment(123456, 24); console.log( `${sentiment.instrumentId}: ${sentiment.sentiment} (${sentiment.buyPercentage}% buy / ${sentiment.sellPercentage}% sell)` ); ``` ## Copy Trading Data Retrieve information about copy trading relationships — who is copying whom, allocation amounts, and copy performance: ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; // Get copy trading stats for a Pro Investor async function getCopyStats() { const response = await fetch(`${API_BASE}/pi-data/copiers`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); const stats = await response.json(); return { totalCopiers: stats.totalCopiers, totalCopiedAmount: stats.totalAllocated, avgCopyDuration: stats.avgDurationDays, topCopyAmounts: stats.copiers ?.slice(0, 5) .map((c) => c.allocatedAmount), }; } ``` ## Best Practices 1. **Respect user privacy** — Only access publicly available profile data. Do not attempt to scrape private information. 2. **Cache aggressively** — Social data changes less frequently than market data. Cache user profiles for 5–10 minutes. 3. **Rate limit awareness** — Social endpoints share the same rate limits as other API surfaces. Batch requests where possible and see the [Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits) for current limits. 4. **Handle missing data** — Not all users have public profiles. Check for null/undefined fields. 5. **Attribution** — If displaying user data publicly, follow eToro's data usage terms. ## Next Steps - [Portfolio Management & Positions](/learn/portfolio-management-and-positions) — Track positions and P&L - [Building an Algo Trading Bot](/learn/building-an-algo-trading-bot) — Automate trading strategies - [Social Feeds API Reference](https://api-portal.etoro.com/api-reference) — Full endpoint documentation --- ### Getting Started with the eToro API URL: https://builders.etoro.com/learn/getting-started-with-etoro-api-v2 Description: Learn how to get your API keys, make your first request, and understand eToro API response formats. ## Prerequisites Before you begin, you'll need: - An eToro account (sign up at [etoro.com](https://www.etoro.com)) - API credentials from the [eToro API Portal](https://api-portal.etoro.com) - A basic understanding of REST APIs - Node.js 18+ or Python 3.8+ installed ## Getting Your API Keys 1. Visit [api-portal.etoro.com](https://api-portal.etoro.com) and sign in with your eToro account 2. Navigate to **Settings → Trading → API Key Management** and click **Create New Key** 3. Copy your **API key** (`x-api-key`) and **User key** (`x-user-key`) 4. Store them securely — never commit API keys to version control > **Tip:** Use the Demo API environment first — it has the same endpoints as production but uses simulated balances, so you can experiment without risk. ## Your First API Request Let's search for instruments by name using the eToro API. ### Using JavaScript (fetch) ```javascript skip-test import { randomUUID } from "node:crypto"; const API_BASE = "https://public-api.etoro.com/api/v1"; const response = await fetch( `${API_BASE}/market-data/search?query=Apple`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), "Accept": "application/json", }, } ); const data = await response.json(); console.log(`Found ${data.results.length} matching instruments`); ``` ### Using cURL ```bash skip-test curl -X GET "https://public-api.etoro.com/api/v1/market-data/search?query=Apple" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" \ -H "x-request-id: $(uuidgen)" \ -H "Accept: application/json" ``` ### Using Python ```python skip-test import os import uuid import requests API_BASE = "https://public-api.etoro.com/api/v1" headers = { "x-api-key": os.environ["ETORO_API_KEY"], "x-user-key": os.environ["ETORO_USER_KEY"], "x-request-id": str(uuid.uuid4()), "Accept": "application/json", } response = requests.get( f"{API_BASE}/market-data/search", params={"query": "Apple"}, headers=headers, ) data = response.json() print(f"Found {len(data['results'])} matching instruments") ``` ## Understanding the Response eToro API responses contain your requested data. Include an `x-request-id` header on every call so support can trace issues back to specific requests. Common HTTP status codes: - **200** — Success - **400** — Bad request (check your parameters) - **401** — Unauthorized (check your API key and user key) - **429** — Rate limited (respect `Retry-After` header and slow down) - **500** — Server error (retry with exponential backoff) ## Rate Limits The eToro API enforces rate limits. Exceeding your limit returns `429 Too Many Requests` with a `Retry-After` header. Always respect that header before retrying. For current rate limit details, see the [Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits). ## What's different from the legacy guide - Uses the `/market-data/search` endpoint for instrument discovery instead of fetching the full instrument list - Adds query parameter patterns for filtering server-side - Same authentication headers (`x-api-key`, `x-user-key`, `x-request-id`) ## Next Steps Now that you've made your first request, explore these guides: - [Real-Time Market Data with WebSockets](/learn/real-time-market-data-websocket) — Stream live prices - [Building an Algo Trading Bot](/learn/building-an-algo-trading-bot) — Automate your strategy - [API Reference](https://api-portal.etoro.com/api-reference) — Full endpoint documentation --- ### Building an Algo Trading Bot URL: https://builders.etoro.com/learn/building-an-algo-trading-bot Description: Build a fully functional algorithmic trading bot using the eToro API with position management, risk controls, and automated strategy execution. ## Overview This guide walks through building an algorithmic trading bot that connects to the eToro API, implements a simple moving average crossover strategy, and manages positions with proper risk controls. We'll start in the demo environment before going live. > **Important:** Always test thoroughly with the Demo Trading API before using real funds. Algorithmic trading carries significant risk. ## Architecture Our bot consists of four main components: 1. **Data collector** — Fetches historical and real-time price data 2. **Strategy engine** — Implements trading logic (SMA crossover) 3. **Order manager** — Executes trades and tracks positions 4. **Risk controller** — Enforces position limits and stop-losses ## Setting Up the Project ```bash skip-test mkdir etoro-trading-bot && cd etoro-trading-bot npm init -y npm install ws node-fetch dotenv ``` Create a `.env` file for your credentials: ```bash skip-test ETORO_API_KEY=your_api_key_here ETORO_USER_KEY=your_user_key_here ETORO_ENVIRONMENT=demo ``` ## The Strategy: SMA Crossover A simple moving average (SMA) crossover strategy generates signals when a fast-period SMA crosses above or below a slow-period SMA: - **Buy signal:** Fast SMA crosses above slow SMA (bullish) - **Sell signal:** Fast SMA crosses below slow SMA (bearish) ```javascript skip-test function calculateSMA(prices, period) { if (prices.length < period) return null; const slice = prices.slice(-period); return slice.reduce((sum, p) => sum + p, 0) / period; } function getSignal(prices, fastPeriod = 10, slowPeriod = 30) { const fastSMA = calculateSMA(prices, fastPeriod); const slowSMA = calculateSMA(prices, slowPeriod); if (!fastSMA || !slowSMA) return "HOLD"; const prevFast = calculateSMA(prices.slice(0, -1), fastPeriod); const prevSlow = calculateSMA(prices.slice(0, -1), slowPeriod); if (!prevFast || !prevSlow) return "HOLD"; if (prevFast <= prevSlow && fastSMA > slowSMA) return "BUY"; if (prevFast >= prevSlow && fastSMA < slowSMA) return "SELL"; return "HOLD"; } ``` ## Order Manager The order manager handles trade execution through the eToro API: ```javascript skip-test import { randomUUID } from "node:crypto"; class OrderManager { constructor(apiKey, userKey, environment) { this.apiBase = "https://public-api.etoro.com/api/v1"; // The unified order endpoint (open/cancel) is served from the v2 API this.executionBase = "https://public-api.etoro.com/api/v2"; this.executionPrefix = environment === "demo" ? "trading/execution/demo" : "trading/execution"; this.apiKey = apiKey; this.userKey = userKey; this.positions = new Map(); } headers() { return { "x-api-key": this.apiKey, "x-user-key": this.userKey, "x-request-id": randomUUID(), "Content-Type": "application/json", }; } async openPosition(instrument, direction, amount) { const response = await fetch( `${this.executionBase}/${this.executionPrefix}/orders`, { method: "POST", headers: this.headers(), body: JSON.stringify({ action: "open", transaction: direction === "BUY" ? "buy" : "sell", instrumentId: instrument, orderType: "mkt", amount: amount, orderCurrency: "usd", leverage: 1, stopLossType: "fixed", }), } ); const order = await response.json(); this.positions.set(instrument, { id: order.positionId, direction, amount, entryPrice: order.executionPrice, }); console.log( `Opened ${direction} position on ${instrument} at ${order.executionPrice}` ); return order; } async closePosition(instrument) { const position = this.positions.get(instrument); if (!position) return null; const response = await fetch( `${this.apiBase}/${this.executionPrefix}/market-close-orders/positions/${position.id}`, { method: "POST", headers: this.headers(), body: JSON.stringify({}), } ); this.positions.delete(instrument); const result = await response.json(); console.log(`Closed position on ${instrument}`); return result; } } ``` ## Risk Controller Never trade without risk controls. Our risk controller enforces: - Maximum position size - Stop-loss percentage - Maximum number of concurrent positions ```javascript skip-test class RiskController { constructor(config) { this.maxPositionSize = config.maxPositionSize || 1000; this.stopLossPercent = config.stopLossPercent || 0.02; this.maxPositions = config.maxPositions || 5; this.currentPositions = 0; } canOpenPosition(amount) { if (amount > this.maxPositionSize) { console.warn(`Position size $${amount} exceeds max $${this.maxPositionSize}`); return false; } if (this.currentPositions >= this.maxPositions) { console.warn(`Max concurrent positions (${this.maxPositions}) reached`); return false; } return true; } shouldStopLoss(entryPrice, currentPrice, direction) { const change = direction === "BUY" ? (currentPrice - entryPrice) / entryPrice : (entryPrice - currentPrice) / entryPrice; return change <= -this.stopLossPercent; } } ``` ## Fetching the Current Price We need a helper to fetch the latest price for an instrument via the REST API: ```javascript skip-test async function getCurrentPrice(instrument) { const response = await fetch( `https://public-api.etoro.com/api/v1/market-data/instruments/rates?instrumentIds=${instrument}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, }, } ); const data = await response.json(); return data.lastPrice; } ``` ## Putting It All Together ```javascript skip-test async function runBot() { const orderManager = new OrderManager( process.env.ETORO_API_KEY, process.env.ETORO_USER_KEY, process.env.ETORO_ENVIRONMENT ); const riskController = new RiskController({ maxPositionSize: 500, stopLossPercent: 0.02, maxPositions: 3, }); const instrument = "AAPL"; const priceHistory = []; // Simulated price feed loop setInterval(async () => { // In production, fetch from WebSocket or REST API const price = await getCurrentPrice(instrument); priceHistory.push(price); // Keep last 100 prices if (priceHistory.length > 100) priceHistory.shift(); const signal = getSignal(priceHistory); console.log(`${instrument}: $${price} | Signal: ${signal}`); if (signal === "BUY" && !orderManager.positions.has(instrument)) { if (riskController.canOpenPosition(500)) { await orderManager.openPosition(instrument, "BUY", 500); riskController.currentPositions++; } } if (signal === "SELL" && orderManager.positions.has(instrument)) { await orderManager.closePosition(instrument); riskController.currentPositions--; } // Check stop-loss const position = orderManager.positions.get(instrument); if ( position && riskController.shouldStopLoss(position.entryPrice, price, position.direction) ) { console.log(`Stop-loss triggered for ${instrument}`); await orderManager.closePosition(instrument); riskController.currentPositions--; } }, 60000); } runBot(); ``` ## Best Practices 1. **Start with demo** — Always validate your strategy in the sandbox first 2. **Log everything** — Record all trades, signals, and errors for analysis 3. **Set hard limits** — Use the risk controller to prevent catastrophic losses 4. **Monitor actively** — Don't leave a bot running unattended for extended periods 5. **Handle errors gracefully** — Network failures, API errors, and edge cases will happen 6. **Backtest first** — Test your strategy against historical data before live trading ## Next Steps - [eToro API Reference](https://api-portal.etoro.com/api-reference) — Complete endpoint docs - [Getting Started](/learn/getting-started-with-etoro-api-v2) — API basics and authentication - [Real-Time Market Data](/learn/real-time-market-data-websocket) — WebSocket streaming guide --- ### Rate Limits & 429 Handling Playbook URL: https://builders.etoro.com/learn/rate-limits-and-429-handling Description: Learn how to handle rate limits gracefully — exponential backoff, Retry-After headers, and best practices for staying within the eToro API's request budgets. ## Overview Every API has rate limits — the eToro API is no exception. This guide covers how to detect, handle, and avoid rate-limit errors so your application stays resilient under load. ## Current rate limits Limits are tracked per user key over a **1-minute rolling window**. | Category | Limit | Applies to | |----------|-------|------------| | **Read (GET)** | **60 requests/min** | Market data, portfolio info, social feeds (read), watchlists (read) | | **Write & Execution** | **20 requests/min** | Trading execution (open, close, cancel), watchlist management (create, update, delete), social feeds (write), user trade info | > **Tip:** Cache non-volatile data locally (instrument metadata, exchange info) to preserve your quota for real-time operations like trading. For the latest details, see the [official Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits). ## What happens when you hit a rate limit When your application exceeds the allowed request rate, the API responds with HTTP `429 Too Many Requests`. The response includes headers that tell you what to do next. ### Key response headers | Header | Meaning | |---|---| | `Retry-After` | Seconds to wait before retrying | | `X-RateLimit-Limit` | Maximum requests allowed in the window | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | > Header availability may vary by endpoint. Check the [API Reference](https://api-portal.etoro.com) for details. ## Exponential backoff with jitter When you receive a `429`, don't retry immediately. Use exponential backoff with jitter to spread retries across time and avoid a "thundering herd" of requests when the window resets. ```python skip-test import time import random import requests def fetch_with_backoff(url, headers, max_retries=5): for attempt in range(max_retries): resp = requests.get(url, headers=headers) if resp.status_code != 429: return resp retry_after = int(resp.headers.get("Retry-After", 2 ** attempt)) jitter = random.uniform(0, retry_after * 0.5) wait = retry_after + jitter print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1})") time.sleep(wait) raise Exception("Max retries exceeded") ``` ```javascript skip-test async function fetchWithBackoff(url, headers, maxRetries = 5) { for (let attempt = 0; attempt < maxRetries; attempt++) { const resp = await fetch(url, { headers }); if (resp.status !== 429) return resp; const retryAfter = parseInt(resp.headers.get("Retry-After") ?? String(2 ** attempt), 10); const jitter = Math.random() * retryAfter * 0.5; const wait = retryAfter + jitter; console.log(`Rate limited. Waiting ${wait.toFixed(1)}s (attempt ${attempt + 1})`); await new Promise((r) => setTimeout(r, wait * 1000)); } throw new Error("Max retries exceeded"); } ``` ## Best practices ### 1. Respect `Retry-After` Always check the `Retry-After` header first. It tells you exactly how long to wait — no guesswork needed. ### 2. Cache when possible Market data that doesn't change frequently (instrument metadata, exchange lists) can be cached locally. This dramatically reduces your request count. ```python skip-test import requests BASE = "https://public-api.etoro.com/api/v1" # Cache instrument types — they rarely change instrument_types = requests.get( f"{BASE}/market-data/instrument-types", headers=headers, ).json() ``` ### 3. Use WebSocket for real-time data Instead of polling `/market-data/instruments/rates` every second, subscribe to price updates via the WebSocket stream. This uses one persistent connection instead of hundreds of HTTP requests. ```javascript skip-test const ws = new WebSocket("wss://ws.etoro.com/ws"); ws.onopen = () => { ws.send(JSON.stringify({ operation: "Authenticate", data: { userKey: USER_KEY, apiKey: API_KEY }, })); ws.send(JSON.stringify({ operation: "Subscribe", data: { topics: ["instrument:100000"], snapshot: false }, })); }; ``` ### 4. Spread requests over time If you need to fetch data for many instruments, don't fire all requests at once. Use a simple rate limiter: ```python skip-test import time instrument_ids = [1001, 1002, 1003, 1004, 1005] for iid in instrument_ids: resp = requests.get( f"{BASE}/market-data/instruments/rates", headers=headers, params={"instrumentIds": iid}, ) print(resp.status_code) time.sleep(0.2) # 5 requests/second ``` ### 5. Monitor your remaining budget Check `X-RateLimit-Remaining` on every response. When it gets low, proactively slow down before you hit the wall. ## Quick reference | Scenario | Recommended action | |---|---| | Got a `429` | Wait for `Retry-After` seconds, then retry with backoff | | `X-RateLimit-Remaining` is low | Slow down request rate proactively | | Need real-time prices | Use WebSocket instead of polling | | Static data (instruments, exchanges) | Cache locally, refresh periodically | | Batch operations | Spread requests over time with a rate limiter | ## Further reading - [Official Rate Limits Documentation](https://api-portal.etoro.com/getting-started/rate-limits) — authoritative numbers and tier details - [Getting Started Guide](/learn/getting-started-with-etoro-api-v2) — set up your API keys - [Real-Time Market Data via WebSocket](/learn/real-time-market-data-websocket) — reduce polling with streams --- ### Real-Time Market Data with WebSockets URL: https://builders.etoro.com/learn/real-time-market-data-websocket Description: Connect to eToro's WebSocket API for live price streaming, handle reconnections, and process real-time market data efficiently. ## Overview The eToro WebSocket API provides real-time streaming of market data including price quotes, order book updates, and trade notifications. This guide walks through connecting, subscribing to channels, and handling data efficiently. ## Connection Setup ### Establishing a WebSocket Connection ```javascript skip-test const WebSocket = require("ws"); const WS_URL = "wss://ws.etoro.com/ws"; function connect(apiKey, userKey) { const ws = new WebSocket(WS_URL, { headers: { "x-api-key": apiKey, "x-user-key": userKey, }, }); ws.on("open", () => { console.log("Connected to eToro WebSocket"); }); ws.on("message", (data) => { const message = JSON.parse(data); handleMessage(message); }); ws.on("close", (code, reason) => { console.log(`Disconnected: ${code} - ${reason}`); if (code !== 1000) { setTimeout(() => connect(apiKey, userKey), 5000); } }); ws.on("error", (error) => { console.error("WebSocket error:", error.message); }); return ws; } ``` ## Subscribing to Channels Once connected, subscribe to specific instrument channels: ```javascript skip-test function subscribe(ws, instruments) { ws.send( JSON.stringify({ action: "subscribe", channels: ["quotes"], instruments: instruments, }) ); } // Subscribe to Apple, Tesla, and Bitcoin subscribe(ws, ["AAPL", "TSLA", "BTC"]); ``` ### Available Channels | Channel | Description | Update Frequency | |---------|-------------|-----------------| | `quotes` | Bid/ask prices | Every tick | | `candles` | OHLCV candles | Per interval | | `orderbook` | Level 2 depth | Every change | | `trades` | Executed trades | Per trade | ## Processing Messages ```javascript skip-test function handleMessage(message) { switch (message.type) { case "quote": console.log( `${message.instrument}: Bid ${message.bid} / Ask ${message.ask}` ); break; case "candle": console.log( `${message.instrument} ${message.interval}: O${message.open} H${message.high} L${message.low} C${message.close}` ); break; case "heartbeat": // Connection keepalive — no action needed break; default: console.log("Unknown message type:", message.type); } } ``` ## Reconnection Strategy Production applications need robust reconnection logic with exponential backoff: ```javascript skip-test class ReconnectingSocket { constructor(url, apiKey, userKey) { this.url = url; this.apiKey = apiKey; this.userKey = userKey; this.attempt = 0; this.maxDelay = 30000; this.subscriptions = []; this.connect(); } connect() { this.ws = new WebSocket(this.url, { headers: { "x-api-key": this.apiKey, "x-user-key": this.userKey, }, }); this.ws.on("open", () => { this.attempt = 0; this.resubscribe(); }); this.ws.on("close", (code) => { if (code !== 1000) { const delay = Math.min( 1000 * Math.pow(2, this.attempt), this.maxDelay ); this.attempt++; console.log(`Reconnecting in ${delay}ms (attempt ${this.attempt})`); setTimeout(() => this.connect(), delay); } }); } subscribe(channels, instruments) { this.subscriptions.push({ channels, instruments }); if (this.ws.readyState === WebSocket.OPEN) { this.ws.send( JSON.stringify({ action: "subscribe", channels, instruments }) ); } } resubscribe() { for (const sub of this.subscriptions) { this.ws.send( JSON.stringify({ action: "subscribe", channels: sub.channels, instruments: sub.instruments, }) ); } } } ``` ## Performance Tips 1. **Batch subscriptions** — Subscribe to multiple instruments in a single message 2. **Throttle UI updates** — Use `requestAnimationFrame` or debounce for rendering 3. **Use binary frames** — Enable MessagePack encoding for lower bandwidth 4. **Unsubscribe** when you no longer need a channel to reduce server load ## Next Steps - [Building an Algo Trading Bot](/learn/building-an-algo-trading-bot) — Use real-time data for automated trading - [WebSocket API Reference](https://api-portal.etoro.com/api-reference) — Full channel documentation - [Getting Started](/learn/getting-started-with-etoro-api-v2) — First steps with the REST API --- ### Authentication & API Keys Deep Dive URL: https://builders.etoro.com/learn/authentication-and-api-keys Description: Master eToro API authentication — API key management, secure storage, token refresh patterns, and troubleshooting common auth errors. ## Overview Every request to the eToro API must be authenticated. This guide covers the authentication model in depth — how keys work, how to store them safely, how to handle token expiration, and what to do when things go wrong. > If you haven't set up API access yet, start with the [Getting Started](/learn/getting-started-with-etoro-api-v2) guide first. ## Authentication Model The eToro API uses a two-key system: | Key | Header | Purpose | |-----|--------|---------| | **Public API Key** | `x-api-key` | Identifies your application | | **User Key** | `x-user-key` | Identifies the acting user | For OAuth-based authentication (used in partner and enterprise integrations), please contact the eToro partnerships team. Both headers are required on every request. Unlike OAuth token flows where tokens expire frequently, eToro API keys are long-lived credentials tied to your account. ```javascript skip-test const headers = { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "Content-Type": "application/json", }; const response = await fetch("https://public-api.etoro.com/api/v1/market-data/instruments", { headers, }); ``` ## Generating API Keys 1. Log in to [api-portal.etoro.com](https://api-portal.etoro.com) 2. Navigate to **Settings → Trading → API Key Management** 3. Click **Create New Key** 4. Copy both the Public API Key and User Key immediately — the User Key is only shown once > **Warning:** Treat your User Key like a password. Anyone with both keys can execute trades on your behalf. ## Secure Key Storage Never hardcode API keys in your source code. Use environment variables or a secrets manager. ### Environment Variables ```bash skip-test # .env file (add to .gitignore!) ETORO_API_KEY=your_public_api_key_here ETORO_USER_KEY=your_user_key_here ETORO_ENVIRONMENT=demo ``` ```javascript skip-test import "dotenv/config"; const config = { apiKey: process.env.ETORO_API_KEY, userKey: process.env.ETORO_USER_KEY, baseUrl: "https://public-api.etoro.com/api/v1", executionPrefix: process.env.ETORO_ENVIRONMENT === "demo" ? "trading/execution/demo" : "trading/execution", }; if (!config.apiKey || !config.userKey) { throw new Error("Missing API credentials. Check your .env file."); } ``` ### Using a Secrets Manager (Production) For production deployments, use a secrets manager instead of `.env` files: ```javascript skip-test // AWS Secrets Manager example import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; async function getCredentials() { const client = new SecretsManagerClient({ region: "us-east-1" }); const response = await client.send( new GetSecretValueCommand({ SecretId: "etoro-api-keys" }) ); return JSON.parse(response.SecretString); } ``` ## Building a Reusable API Client Wrap authentication logic in a client class to avoid repeating headers: ```javascript skip-test class EtoroClient { constructor({ apiKey, userKey, environment = "demo" }) { this.baseUrl = "https://public-api.etoro.com/api/v1"; this.headers = { "x-api-key": apiKey, "x-user-key": userKey, "Content-Type": "application/json", }; } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const response = await fetch(url, { ...options, headers: { ...this.headers, ...options.headers }, }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new ApiError(response.status, error.message || response.statusText); } return response.json(); } async getInstruments(params = {}) { const query = new URLSearchParams(params).toString(); return this.request(`/market-data/instruments${query ? `?${query}` : ""}`); } async getPortfolio() { return this.request("/trading/info/portfolio"); } } class ApiError extends Error { constructor(status, message) { super(`API Error ${status}: ${message}`); this.status = status; } } ``` Usage: ```javascript skip-test const client = new EtoroClient({ apiKey: process.env.ETORO_API_KEY, userKey: process.env.ETORO_USER_KEY, }); const instruments = await client.getInstruments({ type: "stock" }); console.log(`Found ${instruments.length} stocks`); ``` ## Rate Limiting and Retry Logic The eToro API enforces rate limits. When exceeded, the API returns `429 Too Many Requests` with a `Retry-After` header. See the [Rate Limits documentation](https://api-portal.etoro.com/getting-started/rate-limits) for current limits and the [Rate Limits & 429 Handling Playbook](/learn/rate-limits-and-429-handling) for implementation patterns. Implement exponential backoff to handle rate limits gracefully: ```javascript skip-test async function requestWithRetry(fn, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { if (error.status === 429 && attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500; console.log(`Rate limited. Retrying in ${Math.round(delay)}ms...`); await new Promise((r) => setTimeout(r, delay)); continue; } throw error; } } } // Usage const data = await requestWithRetry(() => client.getInstruments()); ``` ## Common Authentication Errors | Status | Error | Cause | Fix | |--------|-------|-------|-----| | `401` | Unauthorized | Missing or invalid API key | Verify `x-api-key` header is set correctly | | `401` | Invalid user key | Wrong or expired user key | Regenerate user key at api-portal.etoro.com | | `403` | Forbidden | Key lacks permission for this endpoint | Check your API key scopes | | `403` | KYC required | Real trading requires identity verification | Complete KYC on etoro.com | | `429` | Too Many Requests | Rate limit exceeded | Implement backoff (see above) | ### Debugging 401 Errors ```javascript skip-test async function debugAuth(apiKey, userKey) { const response = await fetch( "https://public-api.etoro.com/api/v1/market-data/instruments?limit=1", { headers: { "x-api-key": apiKey, "x-user-key": userKey, }, } ); console.log("Status:", response.status); console.log("Headers:", Object.fromEntries(response.headers)); if (!response.ok) { const body = await response.text(); console.log("Error body:", body); } else { console.log("Authentication successful!"); } } ``` ## Key Rotation Best Practices 1. **Rotate keys regularly** — Generate new keys every 90 days 2. **Use separate keys per environment** — Demo and production should use different key pairs 3. **Revoke old keys immediately** — After rotation, delete the previous key from api-portal.etoro.com 4. **Monitor for unauthorized usage** — Log all API calls and alert on unexpected patterns 5. **Never share keys across teams** — Each developer or service should have its own key pair ## Next Steps - [Building an Algo Trading Bot](/learn/building-an-algo-trading-bot) — Put your authenticated client to work - [Real-Time Market Data via WebSocket](/learn/real-time-market-data-websocket) — Stream live data with authenticated connections - [API Reference](https://api-portal.etoro.com/api-reference) — Full endpoint documentation --- ## Blog ### Building a Trader Leaderboard with the Rankings API URL: https://builders.etoro.com/blog/building-a-trader-leaderboard Description: 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. ```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%`. ```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. ```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. --- ### Exploring eToro Social Trading Data URL: https://builders.etoro.com/blog/exploring-etoro-social-trading-data Description: Walk through the social and user data endpoints to build a simple trader leaderboard using the eToro API. Social trading blends market data with **people data**: who is posting, how they have performed, and what the community is discussing. This guide tours the **Social Feeds** and **Users Info** surfaces of the eToro API, then stitches them into a **minimal leaderboard** that ranks traders by a published performance metric. All REST examples use `https://public-api.etoro.com/api/v1/` and send `x-api-key`, optional `x-user-key` when acting as a logged-in user, and a unique `x-request-id` per call. ## Fetching a public user profile Start with a known **user ID** or username from the portal or your app. A profile GET returns display name, avatar URL, bio, and flags such as whether the user is a **Pro Investor**. ```bash curl -sS "https://public-api.etoro.com/api/v1/user-info/people?usernames=jaynemesis" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-request-id: $(uuidgen)" \ -H "accept: application/json" ``` The same call in **JavaScript** is useful when you are assembling leaderboard rows in a serverless function or Node script. ```javascript import { randomUUID } from "node:crypto"; const BASE = "https://public-api.etoro.com/api/v1"; async function fetchProfile(username) { const res = await fetch(`${BASE}/user-info/people?usernames=${username}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY ?? "", "x-request-id": randomUUID(), accept: "application/json", }, }); if (!res.ok) throw new Error(`profile ${username}: ${res.status}`); const { data } = await res.json(); return data; } ``` ## Pulling performance statistics Leaderboards need numbers. Call a **performance** endpoint that returns time-windowed returns, risk score, and max drawdown. Cache aggressively—many consumers poll too often; respect rate limits and ETag headers if provided. ```javascript async function fetchPerformance(username) { const res = await fetch( `${BASE}/user-info/people/${username}/gain`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY ?? "", "x-request-id": randomUUID(), }, } ); if (!res.ok) throw new Error(`performance ${username}: ${res.status}`); const { data } = await res.json(); return { returnPct: data.returnPercent, risk: data.riskScore, copiers: data.activeCopiers, }; } ``` ## Reading the social feed Social feeds provide qualitative context—posts, instruments mentioned, and engagement. Use cursor parameters for pagination and stop when the API returns an empty `nextCursor`. ```bash curl -sS "https://public-api.etoro.com/api/v1/feeds/markets/100000?take=20" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" \ -H "x-request-id: $(uuidgen)" ``` In JavaScript, map feed items to `{ userId, text, instruments }` and join against the profile and performance helpers above if you want a “**signal strength**” panel next to raw posts. ```javascript async function fetchFeedPage(marketId, cursor) { const qs = new URLSearchParams({ take: "20" }); if (cursor) qs.set("cursor", cursor); const res = await fetch(`${BASE}/feeds/markets/${marketId}?${qs}`, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), }, }); if (!res.ok) throw new Error(`feed: ${res.status}`); return res.json(); } ``` ## Building a simple leaderboard Given an array of candidate **usernames** (for example Pro Investors you care about), send one request to the v2 bulk Rankings endpoint. It accepts up to 100 usernames, preserves their order, and silently omits missing, private, or unranked users. ```javascript async function buildLeaderboard(usernames) { const res = await fetch( "https://public-api.etoro.com/api/v2/portfolios/rankings/multiple?period=CurrYear", { method: "POST", headers: { ...usersHeaders(), "content-type": "application/json", }, body: JSON.stringify({ usernames: usernames.slice(0, 100) }), } ); if (!res.ok) throw new Error(`rankings: ${res.status}`); const { results } = await res.json(); const rows = results.map(({ username, value }) => ({ username, name: value.fullName ?? username, returnCurrentYear: value.gain * 100, risk: value.riskScore, copiers: value.copiers, })); return rows.sort((a, b) => b.returnCurrentYear - a.returnCurrentYear); } ``` ## Errors, rate limits, and retries Production scripts should treat **429 Too Many Requests** and **5xx** responses as transient: sleep with jitter, respect `Retry-After` when present, and cap total attempts. For **404** on optional resources (for example a user who closed their public profile), degrade gracefully instead of failing the whole leaderboard job. Log the upstream `x-request-id` together with your own correlation id so support can match client logs to server traces. The bulk Rankings endpoint removes the profile/performance fan-out for up to 100 usernames. If you add optional portfolio or feed enrichment, still use **fixed concurrency** instead of unbounded `Promise.all` across every result. ## Responsible use Social data can include personal information—store only what you need, honor **opt-out** and **regional** restrictions shown in the developer terms, and never present rankings as investment advice. When in doubt, display disclaimers and link to the trader’s full statistics in the eToro client. From this foundation you can add **copy-trading eligibility** checks, filter by instrument, or combine feed sentiment with your own risk models—still using the same header and base-URL conventions throughout. --- ### I built HiveToro to make Community Analytics easier to read URL: https://builders.etoro.com/blog/hivetoro Description: See what the eToro community is actually buying and holding — distilled into one live score. I did not start with a perfect roadmap for HiveToro. I started with a simple product tension: Active eToro investors and traders looking for data-driven community insight needed a clearer way to understand what mattered without doing all the interpretation work themselves. TL;DR: HiveToro shows how a focused eToro app can turn one specific user pain into a useful developer story. Ranks every eToro asset by live community signal — holdings, 24 h/7 d buyer momentum, and Popular Investor weight — into one clear 0–100 score. The first version works because it keeps the skeleton small, the benefit concrete, and the compliance framing clear. ![HiveToro - Abstract builder visual for the app](/blog-assets/hivetoro/direction-3-hero.jpg) ## The pain that started the build The problem was not that users lacked information. The problem was that too much information arrived without enough context. HiveToro was designed around a narrower goal: Know which assets the eToro community is piling into, at a glance, without digging through the feed. That gave the build a clear centre of gravity from day one. For developers and vibe coders, that distinction matters. A good eToro app does not need to cover every workflow at launch. It needs to make one high-friction moment easier to understand, then prove that the moment is worth building around. ## I built a skeleton, not a roadmap The first version needed only a few decisions to feel complete. It had to explain the value quickly, serve Active eToro investors and traders looking for data-driven community insight, and make the next step obvious. App covers US stocks, global stocks, ETFs, and crypto. Scores update in real time via the eToro API. Tone: sharp, data-confident, community-native. ![HiveToro - Product interface scene for the app](/blog-assets/hivetoro/direction-1-product.jpg) That is the useful part of vibe coding: speed is not the point by itself. The point is keeping enough structure that the assistant, the product, and the builder are all solving the same problem. ## How it works on eToro HiveToro fits the eToro builder pattern because it turns platform context into a product moment. Depending on the final app shape, that might mean using eToro APIs, the hosted MCP documentation server, AppStore distribution, or market, portfolio, social, watchlist, and trading surfaces to create a more useful workflow. For this launch, the useful details are: - Live eToro API data - Real-time scoring - Covers stocks, ETFs & crypto ![HiveToro - User moment for the app](/blog-assets/hivetoro/direction-2-user-moment.jpg) ## What the build taught me First, the product story gets stronger when the builder starts with pain instead of features. HiveToro is easier to explain because it is anchored in the user outcome: Know which assets the eToro community is piling into, at a glance, without digging through the feed. Second, the skeleton matters more than the wish list. A tight first version gives the AI coding flow clearer constraints, and it gives reviewers something real to react to. Third, the eToro layer should be visible in the build story. Developers need to understand not only what the app does, but why building it on eToro makes the workflow more practical. ## What builders can do next HiveToro is a useful example for builders who want to ship quickly without turning the first version into a feature maze. Start with one painful moment, map the smallest product loop, then use the eToro Builders Portal to connect the right API, docs, and distribution path. Try HiveToro: https://hivetoro.etoro.app/ ## FAQ ### Who is HiveToro for? HiveToro is built for Active eToro investors and traders looking for data-driven community insight. It is most useful when the reader wants a clearer product context before deciding what to explore next. ### What can developers learn from this build? Developers can learn to start with one real user pain, keep the first version small, and make the eToro platform choice part of the product story rather than a hidden implementation detail. ### Does this post provide financial advice? No. The app and this post provide product context and educational information only. They do not provide financial product advice or any recommendation. ## Disclosures Capital is at risk. This content is intended for information and educational purposes only and should not be considered financial product advice or any recommendation. *Region-specific entity disclaimer (CySEC 109/10 / FCA FRN 583263 / ASIC AFSL 491139 / FSAS SD076) to be resolved from the eToro disclaimer matrix before publish.* --- ### Building Your First Trading Bot with the eToro API URL: https://builders.etoro.com/blog/building-your-first-trading-bot Description: A step-by-step guide to building a simple trading bot using the eToro Demo Trading API. Automated trading on a demo account is the safest way to learn how orders, risk, and execution behave before you put real capital at risk. This tutorial walks you through a minimal **Node.js** bot that authenticates against the eToro Demo Trading API, polls instrument rates, applies a simple **moving average crossover** rule, places market orders when the signal flips, and periodically reconciles open positions. You will need an API key with demo trading enabled, and a user key that identifies the demo portfolio. Store both in environment variables and never commit them to source control. ## Project setup Create a new folder, run `npm init -y`, and add a single dependency if you want structured logging (`pino`) or use `console.log` for simplicity. Your `.env` file should define `ETORO_API_KEY`, `ETORO_USER_KEY`, and optionally `ETORO_INSTRUMENT_ID` for the instrument you want to trade in demo mode. Most HTTP calls in this tutorial use the public API base URL `https://public-api.etoro.com/api/v1/`. The one exception is order execution: the unified order endpoint is served from `https://public-api.etoro.com/api/v2/`. Each request should send a unique `x-request-id` for traceability (UUIDs work well), your `x-api-key`, and when the endpoint acts on behalf of a user, the `x-user-key` header. ## Authenticating and health-checking the session Before placing trades, confirm that your credentials are accepted by calling a lightweight **session** or **account** endpoint. The pattern below is reusable for any authenticated GET. ```javascript import { randomUUID } from "node:crypto"; const BASE = "https://public-api.etoro.com/api/v1"; function headers() { return { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "x-request-id": randomUUID(), "content-type": "application/json", }; } async function verifyDemoSession() { const res = await fetch(`${BASE}/trading/info/demo/portfolio`, { headers: headers() }); if (!res.ok) throw new Error(`Session check failed: ${res.status}`); return res.json(); } ``` If this call returns account metadata (currency, buying power, open P/L), you are ready to pull market data and send orders. ## Fetching instrument rates for your strategy For a moving average crossover you need a rolling window of **mid prices** or **last trade** prices. Poll an instrument rates endpoint at a sensible interval (for example every 15–60 seconds on demo), append each sample to an in-memory array, and trim the array to the longest period you need (for instance 50 closes for a 50-period slow average). ```javascript const closes = []; const FAST = 10; const SLOW = 30; function sma(period) { if (closes.length < period) return null; const slice = closes.slice(-period); return slice.reduce((a, b) => a + b, 0) / period; } async function fetchLastClose(instrumentId) { const res = await fetch( `${BASE}/market-data/instruments/rates?instrumentIds=${instrumentId}`, { headers: headers() } ); if (!res.ok) throw new Error(`Rates failed: ${res.status}`); const body = await res.json(); const last = body.data?.candles?.at(-1); return last?.close ?? last?.mid; } ``` In production you might replace polling with WebSocket candles; for a first bot, polling keeps the control flow linear and easier to debug. ## Crossover logic and order placement When the fast SMA crosses **above** the slow SMA, treat it as a **buy** signal; when it crosses **below**, treat it as a **sell** or **close long** signal depending on your demo rules. Keep position sizing conservative: pass a fixed notional or a fraction of buying power, and always send an idempotency-friendly client reference in the body if the API supports it. ```javascript import { randomUUID } from "node:crypto"; let lastSignal = "flat"; function crossoverSignal() { const fast = sma(FAST); const slow = sma(SLOW); if (fast == null || slow == null) return "hold"; if (lastSignal !== "long" && fast > slow) return "buy"; if (lastSignal === "long" && fast < slow) return "sell"; return "hold"; } async function placeMarketOrder({ instrumentId, side, amount }) { // The unified order endpoint is served from the v2 API const res = await fetch( "https://public-api.etoro.com/api/v2/trading/execution/demo/orders", { method: "POST", headers: headers(), body: JSON.stringify({ action: "open", transaction: side === "BUY" ? "buy" : "sell", instrumentId, orderType: "mkt", amount, orderCurrency: "usd", leverage: 1, stopLossType: "fixed", }), } ); if (!res.ok) { const errText = await res.text(); throw new Error(`Order failed ${res.status}: ${errText}`); } return res.json(); } ``` ## Monitoring positions After each order, poll **open positions** to confirm fills, average price, and unrealized P/L. Use the same header helper so support can correlate logs with `x-request-id`. ```javascript async function listOpenPositions() { const res = await fetch(`${BASE}/trading/info/demo/portfolio`, { headers: headers(), }); if (!res.ok) throw new Error(`Positions failed: ${res.status}`); return res.json(); } async function tick(instrumentId) { const price = await fetchLastClose(instrumentId); if (typeof price === "number") closes.push(price); const signal = crossoverSignal(); if (signal === "buy") { await placeMarketOrder({ instrumentId, side: "BUY", amount: 100 }); lastSignal = "long"; } else if (signal === "sell" && lastSignal === "long") { await placeMarketOrder({ instrumentId, side: "SELL", amount: 100 }); lastSignal = "flat"; } const book = await listOpenPositions(); console.log("signal=%s positions=%j", signal, book); } ``` ## Operational tips Rate-limit your polling loop, handle HTTP 429 with exponential backoff, and log the **request id** whenever you escalate a support ticket. Demo trading mirrors many production constraints but not slippage or liquidity perfectly—treat results as educational, not as a guarantee of live performance. From here you can add stop-loss and take-profit orders, multi-instrument portfolios, or swap polling for streaming data. The same authentication and header discipline applies across those upgrades. --- ### A Developer's Guide to eToro Instrument Discovery URL: https://builders.etoro.com/blog/developers-guide-to-instrument-discovery Description: How to programmatically search, filter, and explore eToro's instrument catalog — asset classes, exchanges, industries, and historical data. Building dashboards, scanners, or trading strategies starts with one question: **which instrument am I actually trading?** eToro exposes a rich **Market Data** surface on the public API so you can resolve symbols to stable IDs, enrich them with exchange and industry metadata, and pull OHLC history for backtests and charting. This guide walks through a practical discovery workflow using `https://public-api.etoro.com/api/v1/` with the headers every integration should send: `x-api-key`, `x-user-key` (when using key-based auth), and a fresh `x-request-id` per call for support correlation. ## Search: from ticker to instrument ID The fastest way to turn a human-readable symbol into an API-ready `instrumentId` is **`GET /market-data/search`**. The endpoint requires a `fields` parameter listing the columns you want back—at minimum include the identifier, symbol, and display name. For an exact match on eToro’s internal symbol, pass `internalSymbolFull` and verify the result before storing the ID in your database. ```bash curl -s -G "https://public-api.etoro.com/api/v1/market-data/search" \ --data-urlencode "internalSymbolFull=AAPL" \ --data-urlencode "fields=instrumentId,internalSymbolFull,displayname,marketId" \ --data-urlencode "pageSize=5" \ -H "x-request-id: $(uuidgen)" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" ``` If you use OAuth instead of API keys, replace the key headers with `Authorization: Bearer `. For broader exploration—sector screens, partial name matches—use `searchText` and paginate with `pageNumber` / `pageSize`. Always request only the fields you need; smaller payloads keep latency predictable when you run searches in a loop. ## Filtering the catalog: types, exchanges, and industries Once you have IDs, **`GET /market-data/instruments`** lets you batch-fetch metadata and apply server-side filters. Typical query parameters include comma-separated **`instrumentIds`**, **`exchangeIds`**, **`stocksIndustryIds`**, and **`instrumentTypeIds`**. That combination is ideal when your app offers filters like “US tech stocks” or “crypto only”: first load reference data, then pass the selected filter IDs. Pull the canonical lists of filter values from the reference endpoints so your UI stays in sync with the backend: - **`GET /market-data/instrument-types`** — asset classes and instrument categories - **`GET /market-data/exchanges`** — optional `exchangeIds` to narrow - **`GET /market-data/stocks-industries`** — optional `stocksIndustryIds` for sector filters ```javascript import { randomUUID } from "node:crypto"; const BASE = "https://public-api.etoro.com/api/v1"; function apiHeaders() { return { "x-request-id": randomUUID(), "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, }; } export async function fetchInstrumentBatch(ids) { const qs = new URLSearchParams({ instrumentIds: ids.join(","), }); const res = await fetch(`${BASE}/market-data/instruments?${qs}`, { headers: apiHeaders(), }); if (!res.ok) throw new Error(`instruments ${res.status}`); return res.json(); } ``` Stash `marketId` when you plan to use instrument feeds or social features that key off market identifiers rather than `instrumentId` alone. ## Latest rates and bulk closing prices For **spot checks** and lightweight pricing widgets, **`GET /market-data/instruments/rates`** accepts comma-separated **`instrumentIds`**. Use it to refresh a watchlist strip or validate that a symbol still trades before placing an order. For **portfolio analytics** and end-of-day reports, **`GET /market-data/instruments/history/closing-price`** returns historical closing prices in bulk—useful when you need a single daily series for many instruments without hammering per-instrument candle endpoints. ## OHLC candles for charts and backtests For charting and strategy research, **`GET /market-data/instruments/{instrumentId}/history/candles/{direction}/{interval}/{candlesCount}`** returns candlesticks. The **`direction`** (`asc` or `desc`), **`interval`** (use values supported by the API for your asset class), and **`candlesCount`** (maximum 1000) are now **path parameters**—for example `/market-data/instruments/1001/history/candles/desc/OneDay/30`. Fetch **descending** when you only need the most recent bars; use **ascending** when you are building a time series from the past forward. ```bash INSTRUMENT_ID=100000 curl -s "https://public-api.etoro.com/api/v1/market-data/instruments/${INSTRUMENT_ID}/history/candles/desc/OneDay/120" \ -H "x-request-id: $(uuidgen)" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" ``` ```javascript export async function fetchRecentDailyCandles(instrumentId, count = 120) { const url = `${BASE}/market-data/instruments/${instrumentId}/history/candles/desc/OneDay/${count}`; const res = await fetch(url, { headers: apiHeaders() }); if (!res.ok) throw new Error(`candles ${res.status}`); const body = await res.json(); return body.data?.candles ?? body.candles ?? body; } ``` Normalize timestamps and timezone assumptions in your app; document which interval you used so backtests stay reproducible. ## Putting it together A solid discovery pipeline looks like this: **search** to resolve symbols, **market-data/instruments** + reference endpoints to filter and enrich, **rates** for live checks, **closing-price** or **candles** for history. Cache reference data (exchanges, industries, types) for a TTL of hours, paginate search aggressively, and always propagate **`x-request-id`** so operations can trace a bad instrument ID or empty candle response end to end. For full request and response schemas, use the official API Portal as the source of truth alongside these patterns. --- ### Vibe Coding with the eToro MCP Server URL: https://builders.etoro.com/blog/vibe-coding-with-etoro-mcp Description: How to use the eToro MCP server with Cursor, Claude, and other AI code editors to build trading apps without writing API calls by hand. You don't need to dig through scattered notes to wire up the eToro API. With the **eToro Public API MCP (Model Context Protocol) server**, your AI editor can discover routes, read the live OpenAPI schema, and execute calls against the real API—while you stay in your project. This is **vibe coding** for fintech. ## What Is MCP? The Model Context Protocol is an open standard that lets AI assistants (like Claude in Cursor, Antigravity, or VS Code) call external tools. Instead of pasting guesses into your codebase, you describe what you want to build, and the assistant pulls structured answers from connected servers. The eToro Public API MCP server gives your AI assistant two tools — `get-all-routes` and `get-route-spec` — that expose the full live OpenAPI document, required scopes, and request/response schemas. Your assistant queries them at inference time to write accurate API code instead of guessing. ## Setting Up ### 1. Install the skill (recommended) The fastest way is to let the skill do the wiring for you. Send this prompt to your agent (Cursor, Claude Code, Antigravity, or any other MCP-capable client): ```text skip-test Please install the skill at https://mcp.public-api.etoro.com/skill and follow the instructions. ``` The skill at [mcp.public-api.etoro.com/skill](https://mcp.public-api.etoro.com/skill) walks your agent through MCP registration for your specific client, authentication, demo vs real accounts, and the safety rules for money-moving routes. ### 2. Or add the MCP server manually The eToro Public API MCP server is hosted — no `npx` package, streamable HTTP transport. If you'd rather wire it up yourself, add this to your editor’s MCP config (e.g. Cursor: `.cursor/mcp.json`): ```json skip-test { "mcpServers": { "etoro-public-api": { "url": "https://mcp.public-api.etoro.com" } } } ``` ### 3. Sign in at the API portal Use [api-portal.etoro.com](https://api-portal.etoro.com) to register your app and get API keys when you call the REST API directly. The MCP server uses your `x-user-key` to scope what your agent can do. ### 3. Start vibing Open your editor and start asking. The assistant can search and read the official API portal documentation through the MCP connection so answers match what the portal publishes. ## Example Workflows ### "Show me the top 5 gaining stocks today" Ask your assistant to find the right Market Data or instrument endpoints, query parameters, and response shapes in the docs, then sketch the HTTP calls or client code. You still run requests against the real API with your own keys; the MCP layer helps the assistant stay aligned with documented behavior instead of inventing endpoints. ### "Build me a portfolio tracker dashboard" Describe the UI and data you need—for example a React view with P&L, allocation, and a sortable positions table. The assistant can look up portfolio-related endpoints, required headers, and pagination from the documentation, then generate components and types that match those contracts. ### "Place a demo trade" Ask the assistant to read the Trading API documentation for order payloads, demo vs. real accounts, and authentication before you implement. That cuts down on mismatches between sample code and the live API. ## Tips for Better Results 1. **Be specific** — "Get the price of Bitcoin" works better than "show me crypto" 2. **Mention demo** — Always specify "demo account" when testing trades 3. **Build incrementally** — Start with data fetching, then add trading logic 4. **Check the types** — Ask the AI to show you the response schema before building UI ## Supported Editors | Editor | MCP Support | Status | |--------|-------------|--------| | Cursor | Native | Fully supported | | Claude Desktop | Native | Fully supported | | VS Code + Continue | Plugin | Supported | | Antigravity | Native | Supported | | Zed | Plugin | Beta | ## Get Started 1. Visit [api-portal.etoro.com](https://api-portal.etoro.com/vibe-code/cursor) for setup instructions 2. Get your API key from the [API Portal](https://api-portal.etoro.com) 3. Start building with your favorite AI editor The future of fintech development is conversational. Start vibing with eToro today. --- ### From Demo to Production: Migrating Your Trading Bot URL: https://builders.etoro.com/blog/from-demo-to-production-trading-bot Description: A practical guide for transitioning your trading bot from eToro's demo sandbox to the real trading API — safety checks, key differences, and best practices. Paper trading proves your signal logic, order sequencing, and state machine; **production** proves your discipline. Moving a bot from eToro’s **virtual** environment to **real** execution is not a find-and-replace on a URL—it is a controlled rollout: separate credentials, different execution paths, stricter risk controls, and observability you can trust when money moves. This guide highlights the differences that matter and shows how to structure configuration so you can switch environments without duplicating your strategy code. ## Demo vs real: endpoints and portfolios The public API is served from `https://public-api.etoro.com/`. Order execution uses the **v2** API while account and market reads stay on **v1**. What changes between sandbox and production is the **path** and the **key environment**. Demo execution lives under paths that include **`demo`** (for example **`POST /api/v2/trading/execution/demo/orders`**), while live trading uses the non-demo execution route (**`POST /api/v2/trading/execution/orders`**). Portfolio and P/L discovery follow the same demo/real split on v1: demo under **`/api/v1/trading/info/demo/...`**, real under **`/api/v1/trading/info/portfolio`**, **`/api/v1/trading/info/real/pnl`**, and related endpoints. Your **User Key** is issued per environment (Virtual vs Real). A key that works for demo portfolio endpoints must not be assumed to work for live trading—store **`ETORO_ENV`** alongside secrets and validate on startup. ## Authentication: OAuth vs API keys Integrations may use **OAuth** (`Authorization: Bearer ...`) or **manual keys** (`x-api-key` + `x-user-key`). The switching logic is the same: production traffic should use **production-issued** tokens or keys, rotated on a schedule and never logged. Whichever method you use, send a unique **`x-request-id`** (UUID) on **every** request so platform logs and your application logs can be joined during an incident. ## Configuration pattern in code Centralize base URL and execution path prefixes so strategy code only calls **`openMarketByAmount(payload)`** and does not embed `/demo/` scattered across files. ```javascript import { randomUUID } from "node:crypto"; const BASE = "https://public-api.etoro.com/api/v1"; /** @type {{ env: "demo" | "real"; apiKey: string; userKey: string }} */ const cfg = { env: process.env.ETORO_ENV === "real" ? "real" : "demo", apiKey: process.env.ETORO_API_KEY, userKey: process.env.ETORO_USER_KEY, }; function authHeaders() { return { "x-request-id": randomUUID(), "x-api-key": cfg.apiKey, "x-user-key": cfg.userKey, "content-type": "application/json", }; } function executionPath(kind) { // Order execution uses the v2 API; account/info reads stay on v1 (BASE) const execBase = "https://public-api.etoro.com/api/v2"; const root = cfg.env === "demo" ? `${execBase}/trading/execution/demo` : `${execBase}/trading/execution`; return `${root}/${kind}`; } export async function openMarketByAmount(body) { const url = executionPath("orders"); const res = await fetch(url, { method: "POST", headers: authHeaders(), body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text(); throw new Error(`openMarket ${res.status}: ${text}`); } return res.json(); } ``` The unified order body uses fields such as **`action`** (`open`/`close`), **`transaction`** (`buy`/`sell`/`sellShort`/`buyToCover`), **`instrumentId`**, **`orderType`** (`mkt`/`mit`), and **`amount`** (or **`units`**)—keep serializers shared between environments so you do not drift. A market-open example: ```json { "action": "open", "transaction": "buy", "instrumentId": 1001, "orderType": "mkt", "amount": 500, "leverage": 1 } ``` ## Portfolio and P/L: match the environment Reconciliation loops should hit the same “side” of the API as execution. Demo positions and P/L come from **`GET /trading/info/demo/portfolio`** and **`GET /trading/info/demo/pnl`**; live accounts use **`GET /trading/info/portfolio`** and **`GET /trading/info/real/pnl`**. The snippet below keeps polling logic identical while swapping only the path—pair it with exponential backoff when you receive **`429`**. ```javascript function tradingInfoPath(resource) { if (cfg.env === "demo") { return resource === "pnl" ? `${BASE}/trading/info/demo/pnl` : `${BASE}/trading/info/demo/portfolio`; } return resource === "pnl" ? `${BASE}/trading/info/real/pnl` : `${BASE}/trading/info/portfolio`; } export async function fetchPortfolioSnapshot() { const res = await fetch(tradingInfoPath("portfolio"), { headers: authHeaders(), }); if (res.status === 429) throw new Error("rate_limited"); if (!res.ok) throw new Error(`portfolio ${res.status}`); return res.json(); } ``` ## Safety checks before going live **Position sizing**: cap **`amount`**, **`units`**, or **`contracts`** with config that is stricter in production than in demo—many incidents are correct logic with wrong magnitude. **Rate limiting**: backoff on **`429`** and avoid tight loops hitting execution or portfolio endpoints; use **`GET /trading/info/portfolio`** (or demo equivalent) at a sane interval, not every tick. **Error handling**: treat **network errors**, **5xx**, and **partial fills** as first-class states; never assume `fetch` success means a fully working order—parse the response body and reconcile open orders and positions. **Staged rollout**: run production keys against **read-only** endpoints first, then enable **small** live notional with manual approval, then widen limits after metrics look stable. ## Monitoring, logging, and rollback Log structured fields: **`x-request-id`**, **`instrumentId`**, order id, position id, and your internal **`strategyRunId`**. Alert on repeated failures, slippage spikes, or divergence between intended and reported positions. Rollback means more than “turn off the bot”: cancel pending orders with **`DELETE /api/v2/trading/execution/orders/{orderId}`** (or the demo equivalent), close or reduce positions via **`market-close-orders`** using **`positionId`**, and disable cron triggers in your scheduler. Keep a **kill switch** environment variable that your process checks before every execution call. ## Summary Migrating from demo to production is primarily about **credential isolation**, **correct execution paths**, and **operational guardrails**—not about rewriting your alpha. Share one codebase, parameterize environment, validate keys on startup, and treat the first week of live trading as a **limited experiment** with tight limits and full observability. For exact path names and body schemas, always cross-check the official API Portal before you deploy. --- ### Building a Real-Time Price Dashboard with the eToro API URL: https://builders.etoro.com/blog/building-a-real-time-price-dashboard Description: An end-to-end tutorial combining the WebSocket API and Market Data endpoints to build a live price dashboard. Live dashboards are where REST meets push: you use **Market Data** endpoints once to resolve instrument metadata, then keep the UI fresh with a **WebSocket** subscription to quote or trade topics. This tutorial shows a small **Node.js** service that connects to the eToro streaming API, subscribes to a handful of instrument IDs, reconnects cleanly after network blips, and merges streaming ticks with REST-backed labels for a simple console or web UI. The REST base URL for examples is `https://public-api.etoro.com/api/v1/`. Streaming uses a separate WebSocket origin at `wss://ws.etoro.com/ws`—refer to the [API Portal](https://api-portal.etoro.com) for the latest connection details. ## Bootstrapping shared headers Whether you call REST or open a WebSocket, reuse the same API identity: `x-api-key`, and when required `x-user-key`. Generate a fresh `x-request-id` per HTTP request; for WebSockets, send an identifying header or query parameter as described in your portal’s auth section. ```javascript import { randomUUID } from "node:crypto"; import WebSocket from "ws"; const REST = "https://public-api.etoro.com/api/v1"; const WS_URL = "wss://ws.etoro.com/ws"; function restHeaders() { return { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY ?? "", "x-request-id": randomUUID(), accept: "application/json", }; } ``` ## Loading instrument metadata over REST Before displaying prices, fetch human-readable names, precision, and exchange session rules so your UI formats decimals correctly and shows **bid/ask** labels consistently. ```javascript async function loadInstrumentMeta(instrumentId) { const res = await fetch(`${REST}/market-data/instruments/${instrumentId}`, { headers: restHeaders(), }); if (!res.ok) throw new Error(`instrument ${instrumentId}: ${res.status}`); const { data } = await res.json(); return { id: instrumentId, symbol: data.symbol, name: data.displayName, pipSize: data.pipSize ?? 0.0001, }; } export async function loadWatchlist(ids) { return Promise.all(ids.map(loadInstrumentMeta)); } ``` You can batch multiple IDs if your portal exposes a bulk instruments route; the pattern stays the same—one REST round-trip, then cache in memory keyed by instrument ID. ## Connecting and subscribing over WebSocket Open the socket with the same API key headers your portal specifies (some deployments use subprotocols or a short-lived ticket in the query string). After `open`, send a **subscribe** message listing topics such as `quotes.{instrumentId}` or `trades.{instrumentId}`. ```javascript function connectSocket({ instrumentIds, onTick }) { const socket = new WebSocket(WS_URL, { headers: { "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY ?? "", "x-request-id": randomUUID(), }, }); socket.on("open", () => { socket.send( JSON.stringify({ type: "subscribe", channels: instrumentIds.map((id) => `quotes.${id}`), }) ); }); socket.on("message", (data) => { const msg = JSON.parse(data.toString()); if (msg.type === "quote") onTick(msg.instrumentId, msg.bid, msg.ask, msg.ts); }); return socket; } ``` ## Reconnection with backoff Networks drop. Wrap the client in a small **reconnect loop**: on `close` or `error`, wait with exponential backoff (1s, 2s, 4s, cap at 30s), then instantiate a new `WebSocket`. Persist the latest known quotes so the UI does not flash empty during reconnect, and resend the subscribe payload on every successful `open`. ```javascript export function resilientStream(instrumentIds, onTick) { let attempt = 0; let socket; const connect = () => { socket = connectSocket({ instrumentIds, onTick }); socket.on("close", scheduleReconnect); socket.on("error", () => socket.close()); }; const scheduleReconnect = () => { const delay = Math.min(30000, 1000 * 2 ** attempt++); setTimeout(() => { connect(); }, delay); }; connect(); return () => socket?.close(); } ``` ## Combining REST metadata with live prices The dashboard model merges static metadata with the latest tick. Below, a tiny in-memory store suitable for pushing to a web client via Server-Sent Events or WebSocket fan-out from your own server. ```javascript const state = new Map(); export function createDashboard(instrumentIds) { const metaPromise = loadWatchlist(instrumentIds); const stop = resilientStream(instrumentIds, (id, bid, ask, ts) => { const row = state.get(id) ?? {}; state.set(id, { ...row, bid, ask, ts }); }); return { async snapshot() { const meta = await metaPromise; for (const m of meta) { const live = state.get(m.id) ?? {}; state.set(m.id, { ...m, ...live }); } return [...state.values()]; }, dispose: stop, }; } ``` ## Display and next steps For a browser UI, expose `snapshot()` on an interval for a table grid, or push diffs when `onTick` fires. Add staleness warnings if `Date.now() - ts` exceeds a few seconds, and fall back to the last REST **mid** price if the stream is quiet during off-hours. You now have a repeatable pattern: **REST for reference data**, **WebSocket for ticks**, **reconnect logic for resilience**. Extend it with order-book depth channels or authenticated user streams when you graduate from read-only market data. --- ### Managing Watchlists at Scale: Tips and Patterns URL: https://builders.etoro.com/blog/managing-watchlists-at-scale Description: Best practices for using the eToro Watchlists API programmatically — curated lists, bulk operations, and organizational patterns. Watchlists are the bridge between **market data** and **human intent**: they group instruments (and sometimes people) so portfolio apps, alerts, and research surfaces stay fast and organized. When you integrate at scale—many users, many lists, frequent updates—you need predictable API usage, idempotent client logic, and a naming strategy that does not collapse under automation. This tutorial focuses on the public API base URL `https://public-api.etoro.com/api/v1/` with **`x-api-key`**, **`x-user-key`**, and **`x-request-id`** on every call. ## Listing and creating watchlists Start by loading the user’s existing lists with **`GET /watchlists`**. Optional query parameters such as **`itemsPerPageForSingle`**, **`ensureBuiltinWatchlists`**, and **`addRelatedAssets`** let you tune payload size; for background sync jobs, prefer smaller page sizes and explicit pagination over giant single responses. Creating a list uses **`POST /watchlists`** with query parameters (not a JSON body): at minimum **`name`**, and optionally **`type`** and **`dynamicQuery`** for dynamic lists. Keep names deterministic when your backend creates lists—e.g. `Sector — US Tech — 2026-Q1`—so duplicate cron runs do not spawn dozens of “My List 7” entries. ```bash curl -s -X POST "https://public-api.etoro.com/api/v1/watchlists?name=Core%20Blue%20Chips&type=User" \ -H "x-request-id: $(uuidgen)" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" ``` Rename with **`PUT /watchlists/{watchlistId}`** using the **`newName`** query parameter, and remove stale lists with **`DELETE /watchlists/{watchlistId}`** once you have confirmed nothing else references them. ## Adding and removing instruments Items are **`WatchlistItemDto`** objects: **`ItemId`** (integer), **`ItemType`** (`Instrument` or `Person`), optional **`ItemRank`**. Use **`POST /watchlists/{watchlistId}/items`** to add, **`PUT`** to reorder or bulk replace (per your integration pattern), and **`DELETE`** to remove. Ranks let you preserve a stable UI order when you sync from an external source of truth. ```javascript import { randomUUID } from "node:crypto"; const BASE = "https://public-api.etoro.com/api/v1"; function headers() { return { "x-request-id": randomUUID(), "x-api-key": process.env.ETORO_API_KEY, "x-user-key": process.env.ETORO_USER_KEY, "content-type": "application/json", }; } /** @param {{ instrumentId: number, rank?: number }[]} rows */ export async function replaceInstrumentItems(watchlistId, rows) { const body = rows.map((r, i) => ({ ItemId: r.instrumentId, ItemType: "Instrument", ItemRank: r.rank ?? i + 1, })); const res = await fetch(`${BASE}/watchlists/${watchlistId}/items`, { method: "PUT", headers: headers(), body: JSON.stringify(body), }); if (!res.ok) throw new Error(`watchlist items ${res.status}: ${await res.text()}`); return res.json(); } ``` Resolve symbols to **`ItemId`** values via **`GET /market-data/search`** before writing items; never hard-code instrument IDs from a spreadsheet without a periodic reconciliation job. ## Fetching items with pagination **`GET /watchlists/{watchlistId}`** supports **`pageNumber`** and **`itemsPerPage`**. For large lists, page through until you receive an empty page or a full count from response metadata—do not assume a fixed number of items per page across API versions. ```bash WATCHLIST_ID=12345 curl -s "https://public-api.etoro.com/api/v1/watchlists/${WATCHLIST_ID}?pageNumber=1&itemsPerPage=50" \ -H "x-request-id: $(uuidgen)" \ -H "x-api-key: $ETORO_API_KEY" \ -H "x-user-key: $ETORO_USER_KEY" ``` ## Curated lists and public watchlists Beyond user-owned lists, product teams often surface **editorial** or **community** content. **`GET /curated-lists`** exposes curated collections for discovery experiences. For a specific user’s public lists—after you have a numeric **`userId`** (CID)—use **`GET /watchlists/public/{userId}`** and **`GET /watchlists/public/{userId}/{watchlistId}`**. Map usernames to IDs with **`GET /user-info/people`** when you only start from a handle. Default-watchlist helpers (`default-watchlist`, `newasdefault-watchlist`, ranking endpoints) matter when your app mirrors eToro’s “primary” list behavior; call them sparingly and only from explicit user actions so you do not fight the user’s own ordering in the mobile app. ## Bulk operations and organization patterns At scale, treat the API as **eventually consistent** with your internal model: queue bulk updates, **deduplicate** by `(watchlistId, ItemId)`, and use a **single writer** per list to avoid last-write-wins races between a web job and a mobile client. Practical patterns: - **Mirror external portfolios**: nightly job resolves symbols → diff against current items → PUT a full ordered set. - **Sector buckets**: one watchlist per sector; bulk add after `/instruments` filter queries. - **Alert fan-out**: store only `watchlistId` + minimal metadata in your service; fetch items when alerts fire. Always backoff on **`429`** responses and log **`x-request-id`** with your internal job ID so support can trace failures. For schema details and additional query flags, refer to the official API Portal—treat these examples as patterns, not an exhaustive contract. ---