Skip to content
Tools

Stake Bet Analyzer

Import your Stake.com archives to see what you played, wagered, won and lost. Your bets are processed on this device. No account needed.

Reads your own Stake.com bet archive, in this tabStake.US is a different site and is not covered.

Import your bets

Get archive → select from Files / Downloads → review summary → view analysis

Processed locally, with a copy kept in this browser. Account saving is optional. No Stake credentials needed.

File limits and privacy details

25 MB per file, 50 MB selected, 64 MB expanded per ZIP, 500 ZIP entries, 100 selected files, 250,000 bets in this tab. Use smaller day archives if a limit is reached.

Start over clears the local copy. Account saving uploads parsed bets only when you choose it. Site usage analytics may run; archive content is excluded from error reports and replay.

Desktop export helper · optional
  1. 1

    Add the export bookmark: drag it, or copy it.

    GAMB·8 Stake Export

    Drag the green button to your bookmarks bar (Ctrl+Shift+B shows the bar; Cmd+Shift+B on a Mac). Some browsers, Arc for one, will not take a dragged bookmark: copy it instead.

    Add it by copying
    1. Press Copy bookmark code.
    2. Right-click your bookmarks bar and choose Add page (Chrome, Edge, Brave) or Add Bookmark (Firefox). In Safari, bookmark any page, then edit it.
    3. Name it GAMB·8 Stake Export and paste the code into the URL field. Save.

    Phone bookmarklet setup and background downloads have not been verified. Use Stake’s own JSON downloads on a phone, or export on a computer and transfer the file.

  2. 2

    Log in to Stake and click the bookmark.

    This optional helper uses your existing Stake session on Stake itself. It attempts to download archive days and supplemental money history, removes IP and account IDs, and saves one JSON file. Stake can change these private endpoints; current logged-in access has not been verified here. It never places bets.

  3. 3

    Choose the file, or drop it here.

    Stake’s own JSON day files work too. Import several together, then add newer archives whenever you want.

Read the bookmark's code before you run it

SHA-256 computing. It talks to the Stake site you are on and nothing else.

/*
 * GAMB·8 Stake archive export (bookmarklet). Source shown on /tools/stake-bet-analyzer.
 *
 * Run it on a Stake tab where you are logged in. It:
 *   1. reads your session token from Stake's own `session` cookie (it is the token Stake's site
 *      sends as x-access-token) and sends it ONLY to the Stake domain you are on;
 *   2. lists your archive days with the same BetArchive query Stake's Transactions page uses;
 *   3. downloads each day from /_api/archive/<id>, two at a time, backing off on 429;
 *   4. removes your IP, user ids and session ids from every row (it keeps the client seed you
 *      chose: with a rotated server seed it lets the analyzer recompute your bets);
 *   5. fetches your deposit and withdrawal history and the credits Stake records outside the bet
 *      archive (rakeback, races, bonuses, tips, rains: type, currency, amount and time only; it
 *      never asks for who sent or received a tip);
 *   6. saves one .json file to your computer. Nothing is sent to GAMB·8 or anyone else.
 * It never bets, clicks or changes anything on your account.
 */
(function () {
  'use strict'
  var VERSION = 1
  var PAGE = 10 // the index page size Stake's own page uses
  var MAX_OFFSET = 1000 // Stake refuses an index offset above 1000 (measured 2026-09-12)
  var CONCURRENCY = 2
  var PII = ['ip', 'userId', 'user_id', 'sessionCasinoUserId', 'sessionProviderUserId', 'clientSeedId']
  var INDEX_QUERY = 'query BetArchive($offset: Int = 0, $limit: Int = 10) { user { id betArchiveList(offset: $offset, limit: $limit) { date count id } } }'
  // Keep in step with TRANSACTION_TYPES in client/utils/stakeArchive/wallet.ts (a test checks).
  var TX_TYPES = ['rakeback', 'racePayout', 'bonusCode', 'dailyBonusClaim', 'depositBonus', 'manualBonus', 'topUpBonusClaim', 'bonusDrop', 'faucetClaim', 'sportsbookPromotionPayout', 'promoCampaignClaim', 'postcardCodeClaim', 'kickRewardClaim', 'mixpanelClaim', 'optimoveClaim', 'tipReceived', 'rainReceived', 'tipSend', 'rainSend']
  var TX_QUERY = 'query AccountTransactions($types: [TransactionTypeEnum!], $offset: Int, $limit: Int) { user { transaction(types: $types, offset: $offset, limit: $limit) { id type currency amount createdAt } } }'
  var userId = null

  if (window.__gamb8ArchiveExport) { window.__gamb8ArchiveExport.show(); return }
  if (!/(^|\.)stake\.[a-z.]+$/i.test(location.hostname)) {
    alert('Open a Stake tab where you are logged in, then click the bookmark there.')
    return
  }
  var m = document.cookie.match(/(?:^|;\s*)session=([^;]+)/)
  if (!m) { alert('No Stake session found on this tab. Log in to Stake first, then click the bookmark again.'); return }
  var token = decodeURIComponent(m[1])

  // ── overlay (DOM calls, not innerHTML: a page enforcing Trusted Types rejects innerHTML) ──
  function el(tag, style, text) {
    var e = document.createElement(tag)
    if (style) e.setAttribute('style', style)
    if (text) e.textContent = text
    return e
  }
  var box = el('div', 'box-sizing:border-box;position:fixed;z-index:2147483647;right:16px;bottom:16px;width:340px;max-width:calc(100vw - 32px);background:#0a0b0e;color:#e8eaed;border:1px solid #00ff88;font:13px/1.45 system-ui,sans-serif;padding:14px 16px;box-shadow:0 8px 30px rgba(0,0,0,.5)')
  box.setAttribute('data-gamb8-export', '')
  var msg = el('div', '', 'Starting…')
  var track = el('div', 'height:4px;background:#222;margin:10px 0')
  var bar = el('div', 'height:4px;width:0;background:#00ff88')
  var cancelBtn = el('button', 'background:#222;color:#e8eaed;border:0;padding:8px 12px;font-weight:700;cursor:pointer', 'Cancel')
  track.appendChild(bar)
  box.appendChild(el('div', 'font:700 11px monospace;letter-spacing:1.2px;color:#00ff88;margin-bottom:6px', 'GAMB·8 ARCHIVE EXPORT'))
  box.appendChild(msg); box.appendChild(track); box.appendChild(cancelBtn)
  document.body.appendChild(box)
  // `stopped` halts every download lane; `cancelled` means the user asked (no error message then).
  var cancelled = false, stopped = false
  cancelBtn.onclick = function () { cancelled = stopped = true; say('Cancelled. Nothing was saved.'); setTimeout(close, 2500) }
  function say(t) { msg.textContent = t }
  function pct(p) { bar.style.width = Math.max(0, Math.min(100, p)) + '%' }
  function close() { box.remove(); delete window.__gamb8ArchiveExport }
  window.__gamb8ArchiveExport = { show: function () { box.style.display = 'block' } }

  function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms) }) }

  async function fetchRetry(url, init, tries) {
    for (var a = 0; a < (tries || 5); a++) {
      if (stopped) throw new Error('cancelled')
      var res = await fetch(url, init)
      if (res.status === 429 || res.status >= 500) { await sleep(2000 * Math.pow(2, a)); continue }
      return res
    }
    throw new Error(url + ' kept failing (rate limited or server error). Try again later; nothing was saved.')
  }

  async function listDays() {
    var days = []
    for (var offset = 0; offset <= MAX_OFFSET; offset += PAGE) {
      say('Listing your archive days… ' + days.length)
      var res = await fetchRetry('/_api/graphql', {
        method: 'POST', credentials: 'include',
        headers: { 'content-type': 'application/json', 'x-access-token': token },
        body: JSON.stringify({ operationName: 'BetArchive', query: INDEX_QUERY, variables: { offset: offset, limit: PAGE } }),
      })
      var json = await res.json()
      if (json && json.data && json.data.user && json.data.user.id) userId = json.data.user.id
      var list = json && json.data && json.data.user && json.data.user.betArchiveList
      if (!list) {
        var why = json && json.errors && json.errors[0] && json.errors[0].message
        throw new Error('Stake did not return your archive list' + (why ? ' (' + why + ')' : '') + '. Are you logged in on this tab?')
      }
      days = days.concat(list)
      if (list.length < PAGE) break
    }
    return days
  }

  function strip(row) {
    var d = row && typeof row === 'object' && row.data && typeof row.data === 'object' ? row.data : row
    for (var i = 0; i < PII.length; i++) { delete d[PII[i]]; if (d !== row) delete row[PII[i]] }
    return row
  }

  // Money in and out. Nothing here may fail the bet export: every problem becomes a note in the file.
  async function fetchWallet() {
    var w = { deposits: null, withdrawals: null, transactions: null, notes: [] }
    if (!userId) { w.notes.push('Stake did not return your account id, so deposits and withdrawals were not fetched.'); return w }
    var kinds = ['deposits', 'withdrawals']
    for (var k = 0; k < kinds.length; k++) {
      say('Fetching your ' + kinds[k] + '…')
      try {
        var res = await fetchRetry('/_api/wallet/' + kinds[k] + '/' + encodeURIComponent(userId), { credentials: 'include', headers: { 'x-access-token': token, accept: '*/*' } }, 3)
        var text = await res.text()
        if (!res.ok) w.notes.push('Stake answered the ' + kinds[k] + ' request with ' + res.status + '.')
        else if (text.trimStart().charAt(0) === '<') w.notes.push('Stake answered the ' + kinds[k] + ' request with a web page (often a Cloudflare check), so it is missing.')
        else w[kinds[k]] = text
      } catch (e) {
        if (stopped) throw e
        w.notes.push('Could not fetch ' + kinds[k] + ': ' + (e && e.message ? e.message : e))
      }
    }
    var tx = []
    try {
      for (var offset = 0, page = 0; page < 400; offset += 50, page++) {
        say('Fetching rakeback, bonuses and tips… ' + tx.length)
        var r = await fetchRetry('/_api/graphql', {
          method: 'POST', credentials: 'include',
          headers: { 'content-type': 'application/json', 'x-access-token': token },
          body: JSON.stringify({ operationName: 'AccountTransactions', query: TX_QUERY, variables: { types: TX_TYPES, offset: offset, limit: 50 } }),
        }, 3)
        var j = await r.json()
        var rows = j && j.data && j.data.user && j.data.user.transaction
        if (!Array.isArray(rows)) {
          var why = j && j.errors && j.errors[0] && j.errors[0].message
          w.notes.push('Stake did not return rakeback and bonus history' + (why ? ' (' + why + ')' : '') + '.')
          break
        }
        for (var i = 0; i < rows.length; i++) {
          var t = rows[i]
          if (t) tx.push({ id: t.id, type: t.type, currency: t.currency, amount: t.amount, createdAt: t.createdAt })
        }
        if (rows.length < 50) break
      }
      w.transactions = tx
    } catch (e) {
      if (stopped) throw e
      w.notes.push('Could not fetch rakeback and bonus history: ' + (e && e.message ? e.message : e))
    }
    return w
  }

  async function run() {
    var days = await listDays()
    if (!days.length) { say('Stake lists no archive days for this account yet.'); return }
    var total = days.reduce(function (s, d) { return s + (d.count || 0) }, 0)
    var parts = new Array(days.length), done = 0, bets = 0, mismatched = 0, next = 0
    async function worker() {
      while (next < days.length && !stopped) {
        var i = next++, d = days[i]
        var res = await fetchRetry('/_api/archive/' + encodeURIComponent(d.id), { credentials: 'include' })
        if (!res.ok) throw new Error('Day ' + d.date + ' returned ' + res.status + '. Nothing was saved.')
        var rows = await res.json()
        if (!Array.isArray(rows)) throw new Error('Day ' + d.date + ' was not a list of bets. Nothing was saved.')
        if (rows.length !== d.count) mismatched++
        parts[i] = '{"date":' + JSON.stringify(d.date) + ',"id":' + JSON.stringify(d.id) + ',"count":' + rows.length + ',"rows":' + JSON.stringify(rows.map(strip)) + '}'
        done++; bets += rows.length
        say('Downloading day ' + done + ' of ' + days.length + ' (' + bets.toLocaleString() + ' bets)')
        pct(100 * done / days.length)
      }
    }
    var ws = []
    for (var k = 0; k < CONCURRENCY; k++) ws.push(worker())
    await Promise.all(ws)
    if (stopped) return
    var wallet = await fetchWallet()
    if (stopped) return
    var head = '{"format":"gamb8-stake-archive","version":' + VERSION + ',"exported_at":' + JSON.stringify(new Date().toISOString()) +
      ',"source":' + JSON.stringify(location.hostname) + ',"index_total":' + total + ',"wallet":' + JSON.stringify(wallet) + ',"days":['
    var blobParts = [head]
    for (var j = 0; j < parts.length; j++) { if (j) blobParts.push(','); blobParts.push(parts[j]) }
    blobParts.push(']}')
    var url = URL.createObjectURL(new Blob(blobParts, { type: 'application/json' }))
    var a = document.createElement('a')
    a.href = url
    a.download = 'stake-archive-' + new Date().toISOString().slice(0, 10) + '.json'
    document.body.appendChild(a); a.click(); a.remove()
    setTimeout(function () { URL.revokeObjectURL(url) }, 60000)
    say('Saved ' + bets.toLocaleString() + ' bets from ' + days.length + ' days' + (mismatched ? ' (' + mismatched + ' days had a different count than Stake listed)' : '') +
      (wallet.notes.length ? '. Some money history is missing: ' + wallet.notes[0] : '') + '. Drop the file on the GAMB·8 analyzer.')
    cancelBtn.textContent = 'Close'
    cancelBtn.onclick = close
  }

  run().catch(function (e) {
    stopped = true // stop the other download lane too
    if (cancelled) return
    say(e && e.message ? e.message : String(e))
    cancelBtn.textContent = 'Close'
    cancelBtn.onclick = close
  })
})()

How it works

Stake's numbers, not ours

Where a bet carries expectedAmount, we use that expected-loss estimate. Missing estimates are reported as unavailable. Net profit/loss is payout minus stake; a returned stake is not profit. Currencies are never converted or added together.

Stays in your browser

Archive contents are parsed locally and kept in this browser for your next visit. Start over clears that local copy. Save to my account explicitly uploads parsed bets, without IP or account IDs, privately for up to 180 days; training use requires a separate opt-in. Site usage analytics may run. This analyzer’s content is blocked from session replay and its error events are excluded from error reporting.

What it can't see

The imported files may omit days or recent bets. An empty calendar cell cannot establish coverage. Account cash flow needs supplemental data. Settled Originals, slots/live games and sportsbook records work when they contain the required fields; unsupported and unsettled records are counted. Stake.US is not covered.

Not a prediction

Good or bad luck is variance, and the edge does not care about it. Nothing here makes the next bet better than its odds. If the totals are not what you want, the cheapest move is to stop.

Keep what you worked out.

A free account saves your bankroll plans, sessions and the results you run here, on every device. No card, no upsell.

Next

Goes with this. House edge and RTP for every Stake game at every setting, side by side: dice, crash, mines, plinko, limbo and more.

Open Stake House Edge Chart →
Related tools