private function handleGuild(DiscordGuild $guild): void {
                $from = now()->sub(1, 'hour');
+               $until = now()->add(14, 'days');
                $eventIDs = $guild->event_subscriptions->pluck('event_id')->toArray();
+               $modeIDs = $guild->ladder_subscriptions->pluck('step_ladder_mode_id')->toArray();
                $userIDs = $guild->user_subscriptions->pluck('user_id')->toArray();
-               if (empty($eventIDs) && empty($userIDs)) {
+               if (empty($eventIDs) && empty($modeIDs) && empty($userIDs)) {
                        return;
                }
 
                $query = Episode::with(['channels', 'event', 'players'])
                        ->where('episodes.start', '>', $from)
+                       ->where('episodes.start', '<', $until)
                        ->orderBy('episodes.start', 'ASC')
                        ->limit(20);
-               $query->where(function ($subquery) use ($eventIDs, $userIDs) {
+               $query->where(function ($subquery) use ($eventIDs, $modeIDs, $userIDs) {
                        if (!empty($eventIDs)) {
                                $subquery->whereIn('episodes.event_id', $eventIDs);
                        }
+                       if (!empty($modeIDs)) {
+                               $subquery->orWhereIn('episodes.step_ladder_mode_id', $modeIDs);
+                       }
                        if (!empty($userIDs)) {
                                $subquery->orWhereHas('players', function ($builder) use ($userIDs) {
                                        $builder->whereIn('episode_players.user_id', $userIDs);
        }
 
        private function handleEpisode(DiscordGuild $guild, Episode $episode): void {
-               $mtime = $episode->updated_at;
-               foreach ($episode->channels as $channel) {
-                       if ($mtime < $channel->updated_at) {
-                               $mtime = $channel->updated_at;
-                       }
-               }
-               foreach ($episode->confirmedCrew() as $crew) {
-                       if ($mtime < $crew->updated_at) {
-                               $mtime = $crew->updated_at;
-                       }
-                       if ($crew->user && $mtime < $crew->user->updated_at) {
-                               $mtime = $crew->user->updated_at;
-                       }
-               }
-               foreach ($episode->players as $player) {
-                       if ($mtime < $player->updated_at) {
-                               $mtime = $player->updated_at;
-                               if ($player->user && $mtime < $player->user->updated_at) {
-                                       $mtime = $crew->user->updated_at;
-                               }
-                       }
-               }
+               $mtime = $episode->getMTime();
                $memo = $this->getMemo($guild, $episode);
                if ((is_null($memo->synced_at) && $episode->start > now()) || (!is_null($memo->synced_at) && $memo->synced_at < $mtime)) {
                        $this->line('pushing '.$episode->id.' '.$episode->getScheduledEventName());
 
                $guild = DiscordGuild::with([
                        'event_subscriptions',
                        'event_subscriptions.event',
+                       'ladder_subscriptions',
+                       'ladder_subscriptions.step_ladder_mode',
                        'user_subscriptions',
                        'user_subscriptions.user',
                ])->where('guild_id', '=', $guild_id)->firstOrFail();
 
                $validatedData = $request->validate([
                        'add_event' => 'numeric|exists:App\Models\Event,id',
+                       'add_ladder' => 'numeric|exists:App\Models\StepLadderMode,id',
                        'add_user' => 'numeric|exists:App\Models\User,id',
                        'remove_event' => 'numeric|exists:App\Models\Event,id',
+                       'remove_ladder' => 'numeric|exists:App\Models\StepLadderMode,id',
                        'remove_user' => 'numeric|exists:App\Models\User,id',
                ]);
 
                if (isset($validatedData['add_event'])) {
                        $guild->event_subscriptions()->create(['event_id' => $validatedData['add_event']]);
                }
+               if (isset($validatedData['add_ladder'])) {
+                       $guild->ladder_subscriptions()->create(['step_ladder_mode_id' => $validatedData['add_ladder']]);
+               }
                if (isset($validatedData['add_user'])) {
                        $guild->user_subscriptions()->create(['user_id' => $validatedData['add_user']]);
                }
                        $sub = $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->firstOrFail();
                        $sub->delete();
                }
+               if (isset($validatedData['remove_ladder'])) {
+                       $sub = $guild->ladder_subscriptions()->where('step_ladder_mode_id', '=', $validatedData['remove_ladder'])->firstOrFail();
+                       $sub->delete();
+               }
                if (isset($validatedData['remove_user'])) {
                        $sub = $guild->user_subscriptions()->where('user_id', '=', $validatedData['remove_user'])->firstOrFail();
                        $sub->delete();
                $guild->load([
                        'event_subscriptions',
                        'event_subscriptions.event',
+                       'ladder_subscriptions',
+                       'ladder_subscriptions.step_ladder_mode',
                        'user_subscriptions',
                        'user_subscriptions.user',
                ]);
 
 namespace App\Http\Controllers;
 
 use App\Models\Event;
-use Carbon\Carbon;
 use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Http\Request;
 
 
--- /dev/null
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\StepLadderMode;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Illuminate\Http\Request;
+
+class StepLadderModeController extends Controller
+{
+
+       public function search(Request $request) {
+               $validatedData = $request->validate([
+                       'exclude_ids' => 'array|nullable',
+                       'exclude_ids.*' => 'int',
+                       'limit' => 'nullable|int',
+                       'phrase' => 'nullable|string',
+               ]);
+               $modes = StepLadderMode::query();
+               if (!empty($validatedData['exclude_ids'])) {
+                       $modes->whereNotIn('id', $validatedData['exclude_ids']);
+               }
+               if (isset($validatedData['limit'])) {
+                       $modes->limit($validatedData['limit']);
+               }
+               if (isset($validatedData['phrase'])) {
+                       $modes->where('name', 'LIKE', '%'.$validatedData['phrase'].'%');
+               }
+               $modes->orderBy('name');
+               return $modes->get()->toJson();
+       }
+
+       public function single(Request $request, StepLadderMode $mode) {
+               $mode->load('description');
+               return $mode->toJson();
+       }
+
+}
 
                return $this->hasMany(DiscordGuildEventSubscription::class);
        }
 
+       public function ladder_subscriptions() {
+               return $this->hasMany(DiscordGuildLadderSubscription::class);
+       }
+
        public function roles() {
                return $this->hasMany(DiscordRole::class)->orderBy('position');
        }
 
--- /dev/null
+<?php
+
+namespace App\Models;
+
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
+use Illuminate\Database\Eloquent\Model;
+
+class DiscordGuildLadderSubscription extends Model {
+
+       use BroadcastsEvents;
+
+       public function broadcastOn(string $event): array {
+               $channels = [];
+               if ($this->discord_guild_id) {
+                       $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id);
+               }
+               return $channels;
+       }
+
+       public function guild() {
+               return $this->belongsTo(DiscordGuild::class);
+       }
+
+       public function step_ladder_mode() {
+               return $this->belongsTo(StepLadderMode::class);
+       }
+
+       protected $fillable = [
+               'discord_guild_id',
+               'step_ladder_mode_id',
+       ];
+
+       protected $with = [
+               'step_ladder_mode',
+       ];
+
+}
 
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
 
 class Episode extends Model
 {
                return $this->belongsTo(StepLadderMode::class);
        }
 
+       public function getMTime() {
+               $mtime = $this->updated_at;
+               foreach ($this->channels as $channel) {
+                       if ($mtime < $channel->updated_at) {
+                               $mtime = $channel->updated_at;
+                       }
+               }
+               foreach ($this->confirmedCrew as $crew) {
+                       if ($mtime < $crew->updated_at) {
+                               $mtime = $crew->updated_at;
+                       }
+                       if ($crew->user && $mtime < $crew->user->updated_at) {
+                               $mtime = $crew->user->updated_at;
+                       }
+               }
+               foreach ($this->players as $player) {
+                       if ($mtime < $player->updated_at) {
+                               $mtime = $player->updated_at;
+                               if ($player->user && $mtime < $player->user->updated_at) {
+                                       $mtime = $crew->user->updated_at;
+                               }
+                       }
+               }
+               return $mtime;
+       }
+
        public function getScheduledEventName(): string {
                $parts = [];
                if (count($this->players) == 4) {
                                }
                        }
                }
+               if ($this->raceroom) {
+                       $description .= "\nRaceroom: [".Str::afterLast($this->raceroom, '/').']('.$this->raceroom.")\n";
+               }
                return $description;
        }
 
 
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void {
+               Schema::create('discord_guild_ladder_subscriptions', function (Blueprint $table) {
+                       $table->id();
+                       $table->foreignId('discord_guild_id')->constrained();
+                       $table->foreignId('step_ladder_mode_id')->constrained();
+                       $table->timestamps();
+                       $table->unique(['discord_guild_id', 'step_ladder_mode_id'], 'guild_mode_unique');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void {
+               Schema::dropIfExists('discord_guild_ladder_subscriptions');
+       }
+};
 
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { Button, Form, ListGroup } from 'react-bootstrap';
+
+import Icon from './Icon';
+import debounce from '../../helpers/debounce';
+
+const StepLadderModeSelect = ({ excludeIds = [], name, onChange, value }) => {
+       const [resolved, setResolved] = useState(null);
+       const [results, setResults] = useState([]);
+       const [search, setSearch] = useState('');
+       const [showResults, setShowResults] = useState(false);
+
+       const ref = useRef(null);
+
+       useEffect(() => {
+               const handleEventOutside = e => {
+                       if (ref.current && !ref.current.contains(e.target)) {
+                               setShowResults(false);
+                       }
+               };
+               document.addEventListener('mousedown', handleEventOutside, true);
+               document.addEventListener('focus', handleEventOutside, true);
+               return () => {
+                       document.removeEventListener('mousedown', handleEventOutside, true);
+                       document.removeEventListener('focus', handleEventOutside, true);
+               };
+       }, []);
+
+       let ctrl = null;
+       const fetch = useCallback(debounce(async phrase => {
+               if (ctrl) {
+                       ctrl.abort();
+               }
+               ctrl = new AbortController();
+               try {
+                       const response = await axios.get(`/api/step-ladder-modes`, {
+                               params: {
+                                       exclude_ids: excludeIds,
+                                       limit: 5,
+                                       phrase,
+                               },
+                               signal: ctrl.signal,
+                       });
+                       ctrl = null;
+                       setResults(response.data);
+                       if (phrase) {
+                               setShowResults(true);
+                       }
+               } catch (e) {
+                       ctrl = null;
+                       console.error(e);
+               }
+       }, 300), [excludeIds]);
+
+       useEffect(() => {
+               fetch(search);
+       }, [search]);
+
+       useEffect(() => {
+               if (value) {
+                       axios
+                               .get(`/api/step-ladder-modes/${value}`)
+                       .then(response => {
+                               setResolved(response.data);
+                       });
+               } else {
+                       setResolved(null);
+               }
+       }, [value]);
+
+       if (value) {
+               return <div className="d-flex justify-content-between">
+                       {resolved ? <span>{resolved.name} ({resolved.ext_id.substr(11)})</span> : <span>value</span>}
+                       <Button
+                               onClick={() => onChange({ target: { name, value: null }})}
+                               size="sm"
+                               variant="outline-danger"
+                       >
+                               <Icon.REMOVE />
+                       </Button>
+               </div>;
+       }
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+               <Form.Control
+                       className="search-input"
+                       name={Math.random().toString(20).substr(2, 10)}
+                       onChange={e => setSearch(e.target.value)}
+                       onFocus={() => setShowResults(true)}
+                       type="search"
+                       value={search}
+               />
+               <div className="search-results-holder">
+                       <ListGroup className="search-results">
+                               {results.map(result =>
+                                       <ListGroup.Item
+                                               action
+                                               key={result.id}
+                                               onClick={() => {
+                                                       onChange({
+                                                               target: { name, value: result.id },
+                                                       });
+                                                       setSearch('');
+                                                       setShowResults(false);
+                                               }}
+                                       >
+                                               <span>{result.name} ({result.ext_id.substr(11)})</span>
+                                       </ListGroup.Item>
+                               )}
+                       </ListGroup>
+               </div>
+       </div>;
+};
+
+StepLadderModeSelect.propTypes = {
+       excludeIds: PropTypes.arrayOf(PropTypes.number),
+       name: PropTypes.string,
+       onChange: PropTypes.func,
+       value: PropTypes.string,
+};
+
+export default StepLadderModeSelect;
 
 import EventSubscriptions from './EventSubscriptions';
 import GuildCrew from './GuildCrew';
 import GuildProtocol from './GuildProtocol';
+import LadderSubscriptions from './LadderSubscriptions';
 import UserSubscriptions from './UserSubscriptions';
 import ErrorBoundary from '../common/ErrorBoundary';
 import { compareTitle } from '../../helpers/Event';
                }
        }, [guild.guild_id, patchGuild, t]);
 
+       const addLadderSub = React.useCallback(async (mode_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               add_ladder: mode_id,
+                       });
+                       patchGuild(response.data);
+               } catch (error) {
+                       toastr.error(t('discordBot.ladderSubError', { error }));
+               }
+       }, [guild.guild_id, patchGuild, t]);
+
+       const removeLadderSub = React.useCallback(async (mode_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               remove_ladder: mode_id,
+                       });
+                       patchGuild(response.data);
+               } catch (error) {
+                       toastr.error(t('discordBot.ladderUnsubError', { error }));
+               }
+       }, [guild.guild_id, patchGuild, t]);
 
        const addUserSub = React.useCallback(async (user_id) => {
                try {
                                        patchGuild(g => ({ ...g, event_subscriptions: (g.event_subscriptions || []).filter(c => c.id !== e.model.id) }));
                                }
                        })
+                       .listen('.DiscordGuildLadderSubscriptionCreated', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: [...g.ladder_subscriptions || [], e.model] }));
+                               }
+                       })
+                       .listen('.DiscordGuildLadderSubscriptionUpdated', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: (g.ladder_subscriptions || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) }));
+                               }
+                       })
+                       .listen('.DiscordGuildLadderSubscriptionDeleted', e => {
+                               if (e.model) {
+                                       patchGuild(g => ({ ...g, ladder_subscriptions: (g.ladder_subscriptions || []).filter(c => c.id !== e.model.id) }));
+                               }
+                       })
                        .listen('.DiscordGuildUserSubscriptionCreated', e => {
                                if (e.model) {
                                        patchGuild(g => ({ ...g, user_subscriptions: [...g.user_subscriptions || [], e.model] }));
                                </Col>
                        </Row>
                        <Row>
-                               <Col md={6}>
+                               <Col className="my-5" md={6}>
                                        <h3>{t('discordBot.eventSubscriptions')}</h3>
                                        <p style={{ minHeight: '3.5em' }}>{t('discordBot.eventSubscriptionDescription')}</p>
                                        <ErrorBoundary>
                                                />
                                        </ErrorBoundary>
                                </Col>
-                               <Col md={6}>
+                               <Col className="my-5" md={6}>
                                        <h3>{t('discordBot.userSubscriptions')}</h3>
                                        <p style={{ minHeight: '3.5em' }}>{t('discordBot.userSubscriptionDescription')}</p>
                                        <ErrorBoundary>
                                                />
                                        </ErrorBoundary>
                                </Col>
+                               <Col className="my-5" md={6}>
+                                       <h3>{t('discordBot.ladderSubscriptions')}</h3>
+                                       <p style={{ minHeight: '3.5em' }}>{t('discordBot.ladderSubscriptionDescription')}</p>
+                                       <ErrorBoundary>
+                                               <LadderSubscriptions
+                                                       addMode={addLadderSub}
+                                                       guild={guild}
+                                                       removeMode={removeLadderSub}
+                                                       subs={guild.ladder_subscriptions || []}
+                                               />
+                                       </ErrorBoundary>
+                               </Col>
                        </Row>
                </section>
                <section className="mt-5">
                })),
                id: PropTypes.number,
                guild_id: PropTypes.string,
+               ladder_subscriptions: PropTypes.arrayOf(PropTypes.shape({
+               })),
                user_subscriptions: PropTypes.arrayOf(PropTypes.shape({
                })),
        }),
 
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import StepLadderModeSelect from '../common/StepLadderModeSelect';
+import Icon from '../common/Icon';
+import { mayManageGuild } from '../../helpers/permissions';
+import { useUser } from '../../hooks/user';
+
+const LadderSubscriptions = ({ addMode, guild, removeMode, subs }) => {
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const mayManage = React.useMemo(() => mayManageGuild(user, guild), [guild, user]);
+
+       return <div>
+               {mayManage ?
+                       <Form.Group controlId="lsubs.addMode">
+                               <Form.Label>{t('discordBot.addLadderMode')}</Form.Label>
+                               <Form.Control
+                                       as={StepLadderModeSelect}
+                                       excludeIds={subs.map(s => s.step_ladder_mode_id)}
+                                       onChange={e => addMode(e.target.value)}
+                                       value=""
+                               />
+                       </Form.Group>
+               : null}
+               {subs.map((lsub) => (
+                       <div className="d-flex align-items-center justify-content-between my-2" key={lsub.id}>
+                               <div>{lsub.step_ladder_mode.name} ({lsub.step_ladder_mode.ext_id.substr(11)})</div>
+                               {mayManage ?
+                                       <div className="button-bar">
+                                               <Button
+                                                       onClick={() => removeMode(lsub.step_ladder_mode_id)}
+                                                       size="sm"
+                                                       title={t('button.remove')}
+                                                       variant="outline-danger"
+                                               >
+                                                       <Icon.DELETE title="" />
+                                               </Button>
+                                       </div>
+                               : null}
+                       </div>
+               ))}
+       </div>;
+};
+
+LadderSubscriptions.propTypes = {
+       addMode: PropTypes.func,
+       guild: PropTypes.shape({
+       }),
+       removeMode: PropTypes.func,
+       subs: PropTypes.arrayOf(PropTypes.shape({
+       })),
+};
+
+export default LadderSubscriptions;
 
                discordBot: {
                        addCrew: 'User hinzufügen',
                        addEvent: 'Event abonnieren',
+                       addLadderMode: 'Ladder Mode abonnieren',
                        addUser: 'User abonnieren',
                        channel: 'Kanal',
                        channelControls: 'Kanal-Steuerung',
                        guildProtocol: 'Command Protokoll',
                        heading: 'Discord Bot',
                        invite: 'Bot einladen',
+                       ladderSubscriptionDescription: 'Races abonnierter Ladder Modi werden als Discord Event angelegt. Einige der Einträge tauchen von Ladderseite aus mehrfach auf. In dem Fall bitte alle Doubletten hinzufügen, sonst wird möglicherweise nur jedes 2. oder 3. Race erkannt.',
+                       ladderSubscriptions: 'Abonnierte Ladder Modi',
                        message: 'Nachricht',
                        messageError: 'Fehler beim Senden',
                        messageSuccess: 'Nachricht in Warteschlange',
 
                discordBot: {
                        addCrew: 'Add user',
                        addEvent: 'Subscribe to event',
+                       addLadderMode: 'Subscribe to ladder mode',
                        addUser: 'Subscribe to user',
                        channel: 'Channel',
                        channelControls: 'Channel controls',
                        guildProtocol: 'Command protocol',
                        heading: 'Discord Bot',
                        invite: 'Invite bot',
+                       ladderSubscriptionDescription: 'Races of subscribed modes will be added as Discord events. Some of the modes have multiple entries on ladder. Please select all dupes, otherwise you may be missing every other or third race in some cases.',
+                       ladderSubscriptions: 'Ladder mode subscriptions',
                        message: 'Message',
                        messageError: 'Error sending message',
                        messageSuccess: 'Message queued',
 
 Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
 Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');
 
+Route::get('step-ladder-modes', 'App\Http\Controllers\StepLadderModeController@search');
+Route::get('step-ladder-modes/{mode}', 'App\Http\Controllers\StepLadderModeController@single');
+
 Route::get('tech', 'App\Http\Controllers\TechniqueController@search');
 Route::get('tech/{tech:name}', 'App\Http\Controllers\TechniqueController@single');