Skip to main content
The Keeper management feature displays approved keeper players for your league, organized by team with previous season comparison and export capabilities.

Viewing Keeper Players

Keeper data is fetched from the Yahoo Fantasy API and organized by team:
1

Navigate to Draft Tab

Click the “Draft” tab in the dashboard, then select the “Keepers” subtab.
2

View Your Keepers

Your team’s keepers appear at the top in a highlighted section with player headshots.
3

Explore League Keepers

Expand the “League Keepers” accordion to see all teams’ keeper selections.

API Endpoint

The keeper data comes from the /api/draft/keepers endpoint:
# main.py:502-695
@app.route("/api/draft/keepers")
def api_draft_keepers():
    if "token" not in session:
        return jsonify({"error": "authentication required"}), 401
    league_key = session.get("league_key")
    if not league_key:
        return jsonify({"error": "no league chosen"}), 400

    log.info("Fetching keeper players for league %s", league_key)

    try:
        # Fetch current season keepers
        keepers_payload = yahoo_api(f"fantasy/v2/league/{league_key}/players;status=K;out=ownership")
        teams_payload = yahoo_api(f"fantasy/v2/league/{league_key}/teams")
    except requests.exceptions.HTTPError as e:
        status_code = e.response.status_code if e.response is not None else 502
        return jsonify({"error": error_detail}), status_code

    teams_meta = _parse_teams_meta(teams_payload)
    keepers = _parse_keeper_players(keepers_payload)

Keeper Player Parsing

The backend parses keeper information including NBA player ID lookup:
# main.py:316-363
def _parse_keeper_players(keepers_payload: Dict[str, Any]) -> List[Dict[str, Any]]:
    players_section = _league_section(keepers_payload, 'players')
    keepers: List[Dict[str, Any]] = []
    
    for key, entry in players_section.items():
        if key == 'count':
            continue
        player_sections = entry.get('player')
        
        # Extract player core data and ownership
        player_core = None
        ownership_blob = None
        if isinstance(player_sections, list):
            for section in player_sections:
                if isinstance(section, list) and player_core is None:
                    player_core = section
                elif isinstance(section, dict) and 'ownership' in section:
                    ownership_blob = section['ownership']
        
        owner_team_key = ownership_blob.get('owner_team_key') if ownership_blob else None
        name_block = _first(player_core, 'name')
        name_full = name_block.get('full') or ' '.join([name_block.get('first'), name_block.get('last')])
        display_position = _first(player_core, 'display_position')
        
        # Look up NBA player ID for headshot images
        nba_player_id = _lookup_nba_player_id(name_full)
        
        keepers.append({
            'player_key': _first(player_core, 'player_key'),
            'player_id': _first(player_core, 'player_id'),
            'name_full': name_full or '',
            'display_position': display_position or '',
            'owner_team_key': owner_team_key,
            'badge': 'Keeper',
            'nba_id': nba_player_id,
            'is_keeper': True,
        })
    return keepers

Previous Season Comparison

The endpoint automatically fetches previous season keepers for comparison:
# main.py:607-646
previous_league_key = _previous_league_key(teams_payload)

if previous_league_key and previous_league_key != league_key:
    log.info("Attempting to fetch previous season keepers via league %s", previous_league_key)
    try:
        prev_keepers_payload = yahoo_api(
            f"fantasy/v2/league/{previous_league_key}/players;status=K;out=ownership"
        )
        prev_teams_payload = yahoo_api(
            f"fantasy/v2/league/{previous_league_key}/teams"
        )
    except Exception as e:
        log.exception("Unexpected error while fetching previous keepers")
    else:
        prev_teams_meta = _parse_teams_meta(prev_teams_payload)
        
        # Match teams across seasons by ID, GUID, or name
        for team in prev_teams_meta:
            team_id = str(team.get('team_id') or '').strip()
            manager_guid = str(team.get('manager_guid') or '').strip()
            team_name = str(team.get('team_name') or '').strip()
            
            if team_id and team_id in id_set:
                team['is_current_login'] = True
            elif manager_guid and manager_guid in guid_set:
                team['is_current_login'] = True
            elif team_name and team_name.casefold() in name_set:
                team['is_current_login'] = True
        
        prev_keepers = _parse_keeper_players(prev_keepers_payload)
        prev_grouped, prev_orphans = _organize_rosters(prev_keepers)
        
        previous_data = {
            'league_key': previous_league_key,
            'season': _league_meta_value(prev_teams_payload, 'season'),
            'teams': prev_teams_meta,
            'keepers_by_team': prev_grouped,
            'orphans': prev_orphans,
        }

Response Structure

The API returns a comprehensive response:
{
  "teams": [
    {
      "team_key": "418.l.12345.t.1",
      "team_name": "My Team",
      "manager_name": "John Doe",
      "is_current_login": true
    }
  ],
  "keepers_by_team": {
    "418.l.12345.t.1": [
      {
        "player_key": "418.p.6583",
        "name_full": "Giannis Antetokounmpo",
        "display_position": "PF,SF",
        "nba_id": "203507",
        "badge": "Keeper"
      }
    ]
  },
  "metadata": {
    "league_key": "418.l.12345",
    "season": "2024",
    "current_keeper_count": 24,
    "previous_keeper_count": 22,
    "previous_season": "2023"
  },
  "previous_season": {
    "season": "2023",
    "keepers_by_team": { /* previous keepers */ }
  }
}

Frontend Rendering

The JavaScript module handles the keeper display:
// keeper.js:89-136
async function loadKeepers() {
  state.loading = true;
  toggleLoading(true);
  showError('');
  
  try {
    const response = await fetch('/api/draft/keepers');
    const payload = await response.json();
    
    if (!response.ok) {
      throw new Error((payload && payload.error) || 'Unable to load keeper data.');
    }

    const normalized = normalizePayload(payload);
    state.data = normalized.current;
    state.previous = normalized.previous;
    state.metadata = normalized.metadata;
    
    // Build user team identification sets
    state.userTeamKeys = new Set(state.metadata.user_team_keys || []);
    state.userTeamIds = new Set(state.metadata.user_team_ids || []);
    state.userTeamNames = new Set(state.metadata.user_team_names || []);
    
    state.loaded = true;
    populateTeamSelect();
    updateScoringNote(state.metadata);
    renderKeepers();
    updateFallbackNotice();
  } catch (error) {
    showError(error.message || 'Unable to load keeper data.');
  } finally {
    state.loading = false;
    toggleLoading(false);
  }
}

Player Headshots

Keeper cards display NBA headshots with fallback initials:
// keeper.js:258-311
function createKeeperHeadshot(player) {
  const wrapper = document.createElement('div');
  wrapper.className = 'keeper-player-photo-wrapper';

  const spinner = document.createElement('div');
  spinner.className = 'keeper-player-photo-spinner';
  wrapper.appendChild(spinner);

  const renderFallback = () => {
    const fallback = document.createElement('div');
    fallback.className = 'keeper-player-photo-fallback';
    fallback.textContent = getKeeperInitials(player && player.name_full);
    wrapper.appendChild(fallback);
  };

  const nbaId = resolveKeeperHeadshotId(player);
  if (nbaId) {
    const image = document.createElement('img');
    image.className = 'keeper-player-photo';
    image.alt = `${player.name_full} headshot`;
    image.loading = 'lazy';
    image.addEventListener('load', () => {
      image.classList.add('keeper-player-photo-visible');
    });
    image.addEventListener('error', renderFallback);
    image.src = `https://cdn.nba.com/headshots/nba/latest/260x190/${nbaId}.png`;
    wrapper.appendChild(image);
  } else {
    renderFallback();
  }

  return wrapper;
}

CSV Export Functionality

Export keeper data for analysis or record-keeping:
// keeper.js:1165-1182
function handleExport() {
  if (!state.data || !DOM.exportBtn || DOM.exportBtn.disabled) return;
  
  const rows = collectExportRows();
  if (!rows.length) return;

  const header = ['Team', 'Manager', 'Player', 'Positions', 'Player Key', 'Player ID'];
  const csvLines = [header, ...rows].map((line) => line.map(formatCsvCell).join(','));
  const csvContent = csvLines.join('\r\n');
  
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  const link = document.createElement('a');
  const seasonSuffix = state.metadata?.season ? `_${state.metadata.season}` : '';
  link.href = window.URL.createObjectURL(blob);
  link.download = `keepers${seasonSuffix}`.replace(/\s+/g, '_') + '.csv';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  window.URL.revokeObjectURL(link.href);
}

Export Data Collection

// keeper.js:1184-1205
function collectExportRows() {
  if (!state.data) return [];
  const rows = [];
  const selectedKey = state.selectedTeam || 'ALL';
  const teams = selectedKey === 'ALL' 
    ? getAugmentedTeamList(state.data) 
    : [state.teamMap[selectedKey]].filter(Boolean);

  teams.forEach((team) => {
    const keepers = state.data.keepersByTeam[team.team_key] || [];
    keepers.forEach((player) => {
      rows.push([
        team.team_name || '',
        team.manager_name || '',
        player.name_full || '',
        player.display_position || '',
        player.player_key || '',
        player.player_id || ''
      ]);
    });
  });

  return rows;
}

Team Filtering

Filter keepers by team using the dropdown:
// keeper.js:65-70
if (DOM.teamSelect) {
  DOM.teamSelect.addEventListener('change', () => {
    state.selectedTeam = DOM.teamSelect.value || 'ALL';
    renderKeepers();
  });
}
The keeper view automatically identifies your team(s) using multiple matching strategies: team key, team ID, manager GUID, and team name comparison.

Fallback Behavior

If no keepers exist for the current season, the previous season is shown:
// keeper.js:731-742
function updateFallbackNotice() {
  if (!DOM.fallbackNotice) return;
  if (!state.metadata || !state.metadata.fallback_to_previous) {
    DOM.fallbackNotice.hidden = true;
    return;
  }
  const currentSeason = state.metadata.season || 'the current season';
  const previousSeason = state.metadata.previous_season || 'last season';
  DOM.fallbackNotice.hidden = false;
  DOM.fallbackNotice.textContent = 
    `No approved keepers yet for season ${currentSeason}. Showing ${previousSeason} keepers below.`;
}
Keeper data requires Yahoo Fantasy API access. If you receive an authentication error, log out and log back in to refresh your session.

Build docs developers (and LLMs) love