Get started

Build a dashboard

Build a page that shows your games’ players live, updating on its own, with each game’s last 7 days.

With AI

Paste this into Claude, ChatGPT or Cursor. It tells the AI everything it needs to build the page on the RoTerminal API.

Paste into your AI
Build me a live dashboard for my Roblox games, as one HTML file with plain JavaScript and no build step. Use the RoTerminal API.

The API
- Base URL: https://roterminal.co/api/v1
- Send my API key with every request: Authorization: Bearer <key>. Put the key in a constant at the top of the file, and I'll paste it in.
- Every response is JSON: { data, link, as_of }. data is the answer, link is the page on roterminal.co with the same numbers, and as_of is when it was made (ISO 8601).
- An error is { error: { code, message } }. Show me error.message.

The endpoints
1. GET /me/games: my games, in data.games. Each has id (its universe ID), name, playing_now and link. Call it once when the page opens. If there are none, tell me to add my games in My Games on roterminal.co.
2. GET /games/{id}/days?days=7: the game's last 7 days, in data.days, newest first. Each day has day (YYYY-MM-DD), average and peak, in players. A field with no value is left out. Call it once for each game when the page opens, and draw a line of the daily average.
3. GET /games/live?games={id},{id}: up to 10 games' players now in one request, in data.games. Each has id, playing_now, place_on_roblox (its rank by players, or null), high_last_24h and at (when it was read, ISO 8601). It doesn't count toward my daily calls. Ask for all my games together every 15 seconds (it allows 12 requests a minute). With more than 10 games, ask for them 10 at a time, taking turns.

Limits
- My plan has a number of calls a day (200 on Free). /me/games and /games/{id}/days count, so never call them on a timer.
- On a 429, wait the number of seconds in the Retry-After header, then carry on.

The page
- A card for each game: its name, linking to its link on roterminal.co; players now, large; its rank on Roblox and its 24-hour high; and the 7-day line.
- When the numbers were last updated, at the top.
- Light and dark mode, and a layout that works on a phone.
- "Numbers from RoTerminal" at the bottom, linking to https://roterminal.co.
- The key is in the file, so the file is for my own computer. If I want to put the dashboard online, the API calls have to move to a small server so the key stays secret: tell me how.

Docs: https://roterminal.co/docs/api.md

When it’s done, paste your API key where the file asks for it, and open the file in your browser.

From a starter file

Save this as dashboard.html, paste your API key on the line that starts with const API_KEY, and open the file in your browser.

dashboard.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My games</title>
  <style>
    :root { color-scheme: light dark; --line: #8884; --accent: #0a84ff; }
    body { max-width: 960px; margin: 0 auto; padding: 32px 16px; font: 15px/1.4 system-ui, sans-serif; }
    header { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
    h1 { margin: 0; font-size: 24px; font-weight: 600; }
    .muted { opacity: 0.6; font-size: 13px; }
    #games { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin: 20px 0; }
    .game { padding: 16px; border: 1px solid var(--line); border-radius: 16px; }
    a { color: inherit; }
    .game a { font-weight: 600; text-decoration: none; }
    .now { margin: 8px 0 2px; font-size: 32px; font-weight: 600; font-variant-numeric: tabular-nums; }
    .now small { font-size: 13px; font-weight: 400; opacity: 0.6; }
    svg { display: block; width: 100%; height: 44px; margin-top: 12px; overflow: visible; }
    polyline { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
  </style>
</head>
<body>
  <header><h1>My games</h1><span class="muted" id="updated"></span></header>
  <div id="games"></div>
  <p class="muted">Numbers from <a href="https://roterminal.co">RoTerminal</a></p>

  <script>
    const API_KEY = 'rta_...' // Create one in Settings → Developer on roterminal.co
    const GAMES = [] // Universe IDs to show. Leave it empty to show your games in My Games.

    async function api(path) {
      const res = await fetch(`https://roterminal.co/api/v1${path}`, { headers: { Authorization: `Bearer ${API_KEY}` } })
      const body = await res.json()
      if (!res.ok) throw Object.assign(new Error(body.error.message), { wait: Number(res.headers.get('retry-after')) })
      return body.data
    }

    const list = document.getElementById('games')
    const fmt = (n) => (n == null ? '–' : n.toLocaleString())
    let games = []

    function card(g) {
      const facts = [g.place && `#${g.place} on Roblox`, g.high && `24h high ${fmt(g.high)}`].filter(Boolean)
      g.el.innerHTML = `<a href="${g.link}" target="_blank"></a>
        <div class="now">${fmt(g.playing_now)} <small>playing now</small></div>
        <div class="muted">${facts.join(' · ')}</div>
        ${g.line?.length > 1 ? spark(g.line) : ''}`
      g.el.querySelector('a').textContent = g.name ?? g.id
    }

    // The last 7 days' average players, oldest first.
    function spark(values) {
      const lo = Math.min(...values)
      const hi = Math.max(...values)
      const points = values.map((v, i) => `${(i / (values.length - 1)) * 100},${42 - ((v - lo) / (hi - lo || 1)) * 40}`)
      return `<svg viewBox="0 0 100 44" preserveAspectRatio="none"><polyline points="${points.join(' ')}" vector-effect="non-scaling-stroke" /></svg>`
    }

    async function days(g) {
      const d = await api(`/games/${g.id}/days?days=7`)
      Object.assign(g, { name: d.name, link: d.link, line: d.days.map((day) => day.average).filter(Boolean).reverse() })
      card(g)
    }

    // Players now for up to 10 games a request, every 15 seconds (it allows
    // 12 a minute). More than 10 games take turns, 10 at a time.
    async function live(turn) {
      const batch = games.slice((turn * 10) % games.length).slice(0, 10)
      let wait = games.length > 10 ? 5 : 15
      try {
        const d = await api(`/games/live?games=${batch.map((g) => g.id).join(',')}`)
        for (const x of d.games) {
          const g = games.find((g) => String(g.id) === String(x.id))
          if (!g) continue
          Object.assign(g, { name: g.name ?? x.name, playing_now: x.playing_now, place: x.place_on_roblox, high: x.high_last_24h })
          card(g)
        }
        document.getElementById('updated').textContent = `Updated ${new Date(d.games[0].at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`
      } catch (err) {
        if (err.wait) wait = err.wait
      }
      setTimeout(() => live(turn + 1), wait * 1000)
    }

    async function start() {
      try {
        games = GAMES.length ? GAMES.map((id) => ({ id, link: `https://roterminal.co/g/${id}` })) : (await api('/me/games')).games
      } catch (err) {
        return (list.textContent = err.message)
      }
      if (!games.length) return (list.textContent = 'There are no games on your account yet. Add yours in My Games on roterminal.co, or put their universe IDs in GAMES.')
      for (const g of games) {
        g.el = list.appendChild(document.createElement('div'))
        g.el.className = 'game'
        card(g)
        days(g).catch(() => {})
      }
      live(0)
    }
    start()
  </script>
</body>
</html>

Your key and your calls

  • Create an API key in Settings → Developer. It’s free.
  • Opening the page uses one call for your games and one for each game’s days. Get live players doesn’t count. You get 200 calls a day on Free, 1,000 on Plus, 2,000 on Pro and 5,000 on Studio. See Rate limits.
  • Get live players allows 12 requests a minute. One request has up to 10 games, so every 15 seconds keeps 10 games live.
  • The key is in the file, so keep the file on your own computer. To put the dashboard online, call the API from your server so the key stays secret. See Authentication.

Just a live number

To show a live player count on your site, you don’t need code or a key. Put your game’s universe ID in one of these:

Widget
<iframe src="https://roterminal.co/embed/7436755782" width="320" height="200" style="border:0" title="Grow a Garden live players"></iframe>
Badge
<a href="https://roterminal.co/g/7436755782"><img src="https://roterminal.co/embed/7436755782.svg" alt="Grow a Garden live players" height="28"></a>

The widget follows your visitor’s light or dark mode. The badge is dark; add ?theme=light to its address for light.