diff --git a/app/styles/play/ladder.sass b/app/styles/play/ladder.sass
new file mode 100644
index 000000000..2821a4098
--- /dev/null
+++ b/app/styles/play/ladder.sass
@@ -0,0 +1,2 @@
+#ladder-view
+  color: black
\ No newline at end of file
diff --git a/app/templates/play/ladder.jade b/app/templates/play/ladder.jade
new file mode 100644
index 000000000..98ee0f316
--- /dev/null
+++ b/app/templates/play/ladder.jade
@@ -0,0 +1,31 @@
+extends /templates/base
+block content
+
+  div#level-column
+    h3= level.get('name')
+    div#level-description
+      !{description}
+  
+  div#leaderboard-column
+    ul.nav.nav-pills
+      for team in teams
+        li
+          a(href="#team-#{team.id}", data-toggle="tab")= team.name
+        
+    .tab-content
+      for team in teams
+        .tab-pane(id="team-#{team.id}")
+          .leaderboard
+            h4 Leaderboard
+            table.table
+              for session in team.leaderboard.topPlayers.models
+                tr= session.get('creatorName')
+          .challengers
+            h4 Challengers
+            
+            if team.easyChallenger
+              div.challenger-select
+                | Easy challenger:
+                button
+                  a(href=link+'?team='+team.id+'&opponent='+team.easyChallenger.id)
+            //TODO: finish once the scoring system is finished
\ No newline at end of file
diff --git a/app/views/play/ladder_view.coffee b/app/views/play/ladder_view.coffee
new file mode 100644
index 000000000..fe98c7b24
--- /dev/null
+++ b/app/views/play/ladder_view.coffee
@@ -0,0 +1,138 @@
+RootView = require 'views/kinds/RootView'
+Level = require 'models/Level'
+LevelSession = require 'models/LevelSession'
+CocoCollection = require 'models/CocoCollection'
+
+HIGHEST_SCORE = 1000000
+
+class LevelSessionsCollection extends CocoCollection
+  url: ''
+  model: LevelSession
+  
+  constructor: (levelID) ->
+    super()
+    @url = "/db/level/#{levelID}/all_sessions"
+    
+class LeaderboardCollection extends CocoCollection
+  url: ''
+  model: LevelSession
+  
+  constructor: (level, options) ->
+    super()
+    options ?= {}
+    @url = "/db/level/#{level.get('original')}.#{level.get('version').major}/leaderboard?#{$.param(options)}"
+
+module.exports = class LadderView extends RootView
+  id: 'ladder-view'
+  template: require 'templates/play/ladder'
+  startsLoading: true
+  
+  constructor: (options, levelID) ->
+    super(options)
+    @level = new Level(_id:levelID)
+    @level.fetch()
+    @level.once 'sync', @onLevelLoaded, @
+    
+    @sessions = new LevelSessionsCollection(levelID)
+    @sessions.fetch({})
+    @sessions.once 'sync', @onMySessionsLoaded, @
+
+  onLevelLoaded: -> @startLoadingPhaseTwoMaybe()
+  onMySessionsLoaded: -> @startLoadingPhaseTwoMaybe()
+
+  startLoadingPhaseTwoMaybe: ->
+    return unless @level.loaded and @sessions.loaded
+    @loadPhaseTwo()
+    
+  loadPhaseTwo: ->
+    alliedSystem = _.find @level.get('systems'), (value) -> value.config?.teams?
+    teams = []
+    for teamName, teamConfig of alliedSystem.config.teams
+      continue unless teamConfig.playable
+      teams.push teamName
+    @teams = teams
+    
+    @leaderboards = {}
+    @challengers = {}
+    for team in teams
+      teamSession = _.find @sessions.models, (session) -> session.get('team') is team
+      @leaderboards[team] = new LeaderboardData(@level, team, teamSession)
+      @leaderboards[team].once 'sync', @onLeaderboardLoaded, @
+      @challengers[team] = new ChallengersData(@level, team, teamSession)
+      @challengers[team].once 'sync', @onChallengersLoaded, @
+    
+  onChallengersLoaded: -> @renderMaybe()
+  onLeaderboardLoaded: -> @renderMaybe()
+
+  renderMaybe: ->
+    loaders = _.values(@leaderboards).concat(_.values(@challengers))
+    return unless _.every loaders, (loader) -> loader.loaded
+    @startsLoading = false
+    @render()
+    
+  getRenderData: ->
+    ctx = super()
+    ctx.level = @level
+    description = @level.get('description')
+    ctx.description = if description then marked(description) else ''
+    ctx.link = "/play/level/#{@level.get('name')}"
+    ctx.teams = []
+    for team in @teams or []
+      ctx.teams.push({
+        id: team
+        name: _.string.titleize(team)
+        leaderboard: @leaderboards[team]
+        easyChallenger: @challengers[team].easyPlayer.models[0]
+        mediumChallenger: @challengers[team].mediumPlayer.models[0]
+        hardChallenger: @challengers[team].hardPlayer.models[0]
+      })
+    ctx
+    
+  afterRender: ->
+    super()
+    @$el.find('#leaderboard-column .nav a:first').tab('show')
+      
+class LeaderboardData
+  constructor: (@level, @team, @session) ->
+    _.extend @, Backbone.Events
+    @topPlayers = new LeaderboardCollection(@level, {order:-1, scoreOffset: HIGHEST_SCORE, team: @team, limit: if @session then 10 else 20})
+    @topPlayers.fetch()
+    @topPlayers.once 'sync', @leaderboardPartLoaded, @
+    
+    if @session
+      score = @session.get('score') or 25
+      @playersAbove = new LeaderboardCollection(@level, {order:1, scoreOffset: score, limit: 4, team: @team})
+      @playersAbove.fetch()
+      @playersAbove.once 'sync', @leaderboardPartLoaded, @
+      @playersBelow = new LeaderboardCollection(@level, {order:-1, scoreOffset: score, limit: 4, team: @team})
+      @playersBelow.fetch()
+      @playersBelow.once 'sync', @leaderboardPartLoaded, @
+
+  leaderboardPartLoaded: ->
+    if @session
+      if @topPlayers.loaded and @playersAbove.loaded and @playersBelow.loaded
+        @loaded = true
+        @trigger 'sync'
+    else
+      @loaded = true
+      @trigger 'sync'
+
+class ChallengersData
+  constructor: (@level, @team, @session) ->
+    _.extend @, Backbone.Events
+    score = @session?.get('score') or 25
+    @easyPlayer = new LeaderboardCollection(@level, {order:1, scoreOffset: score - 5, limit: 1, team: @team})
+    @easyPlayer.fetch()
+    @easyPlayer.once 'sync', @challengerLoaded, @
+    @mediumPlayer = new LeaderboardCollection(@level, {order:1, scoreOffset: score, limit: 1, team: @team})
+    @mediumPlayer.fetch()
+    @mediumPlayer.once 'sync', @challengerLoaded, @
+    @hardPlayer = new LeaderboardCollection(@level, {order:-1, scoreOffset: score + 5, limit: 1, team: @team})
+    @hardPlayer.fetch()
+    @hardPlayer.once 'sync', @challengerLoaded, @
+
+  challengerLoaded: ->
+    if @easyPlayer.loaded and @mediumPlayer.loaded and @hardPlayer.loaded
+      @loaded = true
+      @trigger 'sync'
+      
\ No newline at end of file
diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee
index ea686a357..8f0bb1fbc 100644
--- a/server/levels/level_handler.coffee
+++ b/server/levels/level_handler.coffee
@@ -23,6 +23,7 @@ LevelHandler = class LevelHandler extends Handler
 
   getByRelationship: (req, res, args...) ->
     return @getSession(req, res, args[0]) if args[1] is 'session'
+    return @getLeaderboard(req, res, args[0]) if args[1] is 'leaderboard'
     return @getAllSessions(req, res, args[0]) if args[1] is 'all_sessions'
     return @getFeedback(req, res, args[0]) if args[1] is 'feedback'
     return @sendNotFoundError(res)
@@ -72,7 +73,14 @@ LevelHandler = class LevelHandler extends Handler
       Session.find(sessionQuery).exec (err, results) =>
         return @sendDatabaseError(res, err) if err
         res.send(results)
-        return res.end()
+        res.end()
+        
+  getLeaderboard: (req, res, id) ->
+    # stub handler
+#    [original, version] = id.split('.')
+#    version = parseInt version
+#    console.log 'get leaderboard for', original, version, req.query
+    return res.send([])
 
   getFeedback: (req, res, id) ->
     @getDocumentForIdOrSlug id, (err, level) =>