API keys are tied to your TimeTrader account. Sign in (the key icon, top-right of any page), then return here to request and manage your keys.
Signed in as —
Request API key
A key authorizes programmatic access to the TimeTrader External API. read keys can fetch races, predictions, odds and your account; read_write keys can also place bets. Every key requires Captain approval before it will work — your request goes to VGhost for authorization.
⚠ Save this now — it will not be shown again. Your key is PENDING Captain approval and will not work until VGhost authorizes it.
This is the only time the raw key is displayed. TimeTrader stores only a hash — if you lose it, revoke it and request a new one. The key stays inactive until it shows approved in Your keys below.
Your keys
Keys must be approved by the Captain before they work. Pending and approved keys can be revoked at any time; revoked keys stop working immediately.
Prefix
Scope
Label
Created
Last used
Status
loading…
♚ Captain's approval queue
Pending API-key requests across all TimeTrader accounts. Approve to activate the key, or deny to reject it. Refreshes automatically.
Requester
Label
Scope
Prefix
Requested
loading…
Using your key
Base URL: /api/v1/ · authenticate with a bearer header. Example:
GET races/{urid}/entries — entries for a race (post, horse)
GET races/{urid}/predictions — agent predictions for a race
GET odds — live odds (?track=&race=)
GET account — your ticks, dream minutes, profile
GET account/bets — your bet history
POST bets — place a bet read_write
Note: new keys require Captain approval before they authenticate — a freshly requested key returns an auth error until VGhost authorizes it (status flips to approved in Your keys above). Responses are JSON: {"ok":true,"data":…} or {"ok":false,"error":"…"}. The MCP server timetrader-mcp wraps these endpoints as tools for AI agents (Claude Desktop / Claude Code) — point it at your approved key and it can read races, odds, your account, and place bets with a read_write key.
, denied=document.getElementById('denied');
var akWho=document.getElementById('akWho');
var btnCreate=document.getElementById('btnCreate'), kLabel=document.getElementById('kLabel'), kScope=document.getElementById('kScope');
var createErr=document.getElementById('createErr');
var rawBox=document.getElementById('rawBox'), rawKey=document.getElementById('rawKey'), copyBtn=document.getElementById('copyBtn');
var keysBody=document.getElementById('keysBody'), listErr=document.getElementById('listErr');
var adminPanel=document.getElementById('adminPanel'), reqBody=document.getElementById('reqBody'), reqErr=document.getElementById('reqErr');
var started=false, isAdmin=false, adminTimer=null;
function esc(t){ return String(t==null?'':t).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
function svcDown(e){ var m=String(e&&e.message||e||''); return /Failed to fetch|NetworkError|ECONN|Load failed/i.test(m); }
function offlineMsg(){ return 'external API offline — the /api/v1 service is not reachable right now. Try again shortly.'; }
function fmtTime(s){ if(!s) return '—'; try{ var d=new Date(s); if(isNaN(d)) return esc(s); return d.toLocaleString([], {year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); }catch(_){ return esc(s); } }
// unwrap {ok:true,data} envelope; throw on {ok:false} or non-2xx
function apiFetch(url, opts){
return fetch(url, Object.assign({credentials:'include'}, opts||{})).then(function(r){
return r.text().then(function(t){
var j=null; try{ j=t?JSON.parse(t):null; }catch(_){ }
if(!r.ok){ throw new Error((j&&j.error)||('HTTP '+r.status)); }
if(j && j.ok===false){ throw new Error(j.error||'request failed'); }
return j ? j.data : null;
});
});
}
// ---- CREATE ----
btnCreate.addEventListener('click', function(){
createErr.textContent=''; rawBox.style.display='none';
btnCreate.disabled=true;
var body={ label:(kLabel.value||'').trim(), scope:kScope.value };
apiFetch('/api/v1/keys', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) })
.then(function(d){
var raw=d&&(d.api_key||d.key||d.raw_key);
if(!raw){ createErr.textContent='key created, but no raw key was returned by the server.'; loadKeys(); return; }
rawKey.textContent=raw;
rawBox.style.display='block';
kLabel.value='';
loadKeys();
})
.catch(function(err){ createErr.textContent= svcDown(err)?offlineMsg():('could not create key — '+esc(err&&err.message||err)); })
.then(function(){ btnCreate.disabled=false; });
});
copyBtn.addEventListener('click', function(){
var txt=rawKey.textContent||'';
function done(){ var o=copyBtn.textContent; copyBtn.textContent='Copied'; setTimeout(function(){ copyBtn.textContent=o; },1400); }
if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(txt).then(done).catch(fallback); }
else fallback();
function fallback(){ try{ var r=document.createRange(); r.selectNodeContents(rawKey); var s=window.getSelection(); s.removeAllRanges(); s.addRange(r); document.execCommand('copy'); done(); }catch(_){ copyBtn.textContent='select & copy'; } }
});
// ---- LIST ----
function loadKeys(){
listErr.textContent='';
apiFetch('/api/v1/keys', { method:'GET' })
.then(function(d){ renderKeys(Array.isArray(d)?d:(d&&d.keys)||[]); })
.catch(function(err){
keysBody.innerHTML='
—
';
listErr.textContent= svcDown(err)?offlineMsg():('could not load keys — '+esc(err&&err.message||err));
});
}
function renderKeys(keys){
if(!keys.length){ keysBody.innerHTML='
No keys yet — create one above.
'; return; }
keysBody.innerHTML='';
keys.forEach(function(k){
var st = String(k.status||'').toLowerCase();
var revoked = !!(k.revoked_at || st==='revoked');
var prefix = k.key_prefix || k.prefix || '—';
var scope = k.scope || 'read';
var tr=document.createElement('tr');
var statusHtml;
if(revoked){ statusHtml='REVOKED'; }
else if(st==='pending'){ statusHtml='PENDING'; }
else if(st==='denied'){ statusHtml='DENIED'; }
else if(st==='approved'){ statusHtml='APPROVED'; }
else { statusHtml='ACTIVE'; }
// Revoke only meaningful for approved/pending (live) keys
var revocable = !revoked && st!=='denied';
var actionHtml = revocable ?
'' : '';
tr.innerHTML =
'
'+esc(prefix)+'
'+
'
'+esc(scope)+'
'+
'
'+esc(k.label||'—')+'
'+
'
'+esc(fmtTime(k.created_at||k.created))+'
'+
'
'+esc(fmtTime(k.last_used_at||k.last_used))+'
'+
'
'+statusHtml+'
'+
'
'+actionHtml+'
';
keysBody.appendChild(tr);
});
Array.prototype.forEach.call(keysBody.querySelectorAll('.revoke-btn'), function(b){
b.addEventListener('click', function(){ revokeKey(b.getAttribute('data-id'), b); });
});
}
function revokeKey(id, btn){
if(!id) return;
if(!window.confirm('Revoke this key? Any client using it will stop working immediately.')) return;
btn.disabled=true; btn.textContent='revoking…'; listErr.textContent='';
apiFetch('/api/v1/keys/revoke', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({api_key_id:isNaN(+id)?id:+id}) })
.then(function(){ loadKeys(); })
.catch(function(err){ btn.disabled=false; btn.textContent='Revoke'; listErr.textContent= svcDown(err)?offlineMsg():('revoke failed — '+esc(err&&err.message||err)); });
}
// ---- ADMIN: CAPTAIN'S APPROVAL QUEUE ----
function loadRequests(){
if(!isAdmin) return;
reqErr.textContent='';
apiFetch('/api/v1/keys/requests', { method:'GET' })
.then(function(d){ renderRequests(Array.isArray(d)?d:(d&&d.requests)||[]); })
.catch(function(err){
reqBody.innerHTML='
—
';
reqErr.textContent= svcDown(err)?offlineMsg():('could not load requests — '+esc(err&&err.message||err));
});
}
function renderRequests(reqs){
if(!reqs.length){ reqBody.innerHTML='
No pending requests — the queue is clear.
'; return; }
reqBody.innerHTML='';
reqs.forEach(function(q){
var who = esc(q.name||'—') + (q.email ? ' <'+esc(q.email)+'>' : '');
var tr=document.createElement('tr');
tr.innerHTML =
'