--:--:--
Connecting...
STARSHIP
offline

API Keys

DN QEm

API Keys

request & manage your TimeTrader External API keys
⌂ home

Sign in

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.

PrefixScopeLabelCreatedLast usedStatus
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.

RequesterLabelScopePrefixRequested
loading…

Using your key

Base URL: /api/v1/ · authenticate with a bearer header. Example:

curl -H "Authorization: Bearer ttk_..." \
     https://www.timetrader.org/api/v1/races

Endpoints:

  • GET races — list races (?date=YYYY-MM-DD&track=)
  • 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 = ''+who+''+ ''+esc(q.label||'—')+''+ ''+esc(q.scope||'read')+''+ ''+esc(q.key_prefix||'—')+''+ ''+esc(fmtTime(q.requested_at))+''+ ''+ ''+ ''+ ''; reqBody.appendChild(tr); }); Array.prototype.forEach.call(reqBody.querySelectorAll('.approve-btn'), function(b){ b.addEventListener('click', function(){ decide('approve', b.getAttribute('data-id'), b); }); }); Array.prototype.forEach.call(reqBody.querySelectorAll('.deny-btn'), function(b){ b.addEventListener('click', function(){ decide('deny', b.getAttribute('data-id'), b); }); }); } function decide(action, id, btn){ if(!id) return; var verb = action==='approve' ? 'Approve' : 'Deny'; if(!window.confirm(verb+' this API-key request?')) return; var row = btn.parentNode; Array.prototype.forEach.call(row.querySelectorAll('button'), function(b){ b.disabled=true; }); btn.textContent = (action==='approve'?'approving…':'denying…'); reqErr.textContent=''; apiFetch('/api/v1/keys/'+action, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({api_key_id:isNaN(+id)?id:+id}) }) .then(function(){ loadRequests(); loadKeys(); }) .catch(function(err){ Array.prototype.forEach.call(row.querySelectorAll('button'), function(b){ b.disabled=false; }); btn.textContent = verb; reqErr.textContent= svcDown(err)?offlineMsg():(action+' failed — '+esc(err&&err.message||err)); }); } function enableAdmin(){ if(isAdmin) return; isAdmin=true; adminPanel.style.display='block'; loadRequests(); if(!adminTimer){ adminTimer=setInterval(loadRequests, 20000); } } function startTool(){ if(started) return; started=true; loadKeys(); } // SESSION GATE: /api/auth/me -> any signed-in user (every TimeTrader user can mint keys) function gate(u){ var signedIn = !!(u && (u.email || u.username || u.user_id || u.id)); main.style.display = signedIn ? 'block' : 'none'; denied.style.display = signedIn ? 'none' : 'block'; if(signedIn){ akWho.textContent = String(u.email||u.username||'(your account)').trim(); startTool(); var st = String(u.status||u.role||'').toLowerCase(); if(st==='admin' || st==='superadmin'){ enableAdmin(); } } } fetch('/api/auth/me',{credentials:'include'}).then(function(r){return r.ok?r.json():null;}).then(function(j){ gate(j&&j.success?j.data:null); }).catch(function(){ gate(null); }); document.addEventListener('starlogin', function(e){ gate(e.detail); }); })();
Dream