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
Add the export bookmark: drag it, or copy it.
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
- Press Copy bookmark code.
- Right-click your bookmarks bar and choose Add page (Chrome, Edge, Brave) or Add Bookmark (Firefox). In Safari, bookmark any page, then edit it.
- 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
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
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
Stays in your browser
What it can't see
Not a prediction
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.
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 →