Documentation

Everything you need to use WastedLightBot, build custom commands, connect Stream Deck, or work directly with the API.

Streamer docs

Use WastedLightBot

Built-in commands, custom command variables, and the basics for getting your channel set up.

Browse streamer docs →
Developer docs

Build with WastedLightBot

Stream Deck integration, API authentication, endpoints, and instructions for building your own plugin.

Browse developer docs →
Streamer docs

Getting Started

Connect WastedLightBot to your Twitch channel, then choose which built-in features you want active from your dashboard.

1. Log inAuthorize the dashboard with your Twitch account.
2. Connect botAuthorize the bot to join your channel and give it permission to read and send messages.
3. Toggle featuresEverything is configurable from the dashboard, so turn off anything you don't want in your chat.

Built-in Commands

These come free with every channel. Toggle any of them off from your dashboard if they're not for your chat.

Loading commands...

Custom Command Variables

When you build a custom command from the dashboard, you can use these variables anywhere in the response text. The bot fills them in automatically when the command runs.

VariableWhat it does
$userThe display name of whoever ran the command.
$channelYour channel's username.
$targetThe user tagged/mentioned in the command, if any.
$argsAny text typed after the command name.
$random(a, b, c)Picks one option at random from a comma-separated list.
$randomnumber(min, max)Generates a random whole number between min and max, inclusive.
$customapi(URL)Fetches a URL and drops its text response into the message. Other variables inside the URL (like $user) get substituted before the request is made. 5 second timeout, response capped at 400 characters. Free on every plan.
!hype $user just hyped the stream to $randomnumber(1, 100)%! $random(LETS GO, POGGERS, no cap)

Building a Custom Command

From your dashboard, go to the Commands tab, scroll to Custom Commands, then fill in:

FieldWhat to put
TriggerThe word chat types, e.g. !hype (must start with !)
ResponseWhat the bot replies with, using any variables from above

Custom commands are unlimited on every plan. If a custom command's trigger matches a built-in command, the built-in one always wins.

Developer docs

Build with WastedLightBot

The developer documentation is for streamers and developers who want to connect WastedLightBot to their own tools, extend the Stream Deck integration, or call the API directly.

Stream Deck

Trigger actions straight from a physical button -- pick a giveaway winner, skip a song, pull the next person from your queue -- without touching the dashboard mid-stream.

Generate an API key from your dashboard's Bot tab, download the plugin, and drag any of the built-in actions onto a button. The key is shared across every button, so you only enter it once.

Want more buttons than the default 3? Every action below works over a simple HTTP request, with your key sent as Authorization: Bearer wlb_yourkeyhere -- if you're comfortable editing a small JavaScript file, you can wire up your own button for anything here.

API endpoints

EndpointBodyWhat it does
POST /giveaway/start{"keyword":"PIZZA"}Starts a giveaway with the given keyword.
POST /giveaway/pick-winner-Picks a random winner and ends the giveaway.
POST /giveaway/reroll-Picks a new winner, excluding the last one.
POST /giveaway/cancel-Ends the giveaway with no winner picked.
POST /queue/open-Opens the viewer queue for new joins.
POST /queue/close-Closes the viewer queue.
POST /queue/next-Pulls the next person in line.
POST /queue/clear-Empties the viewer queue.
GET /queue/status-Returns { open, length }.
POST /song/skip-Skips the current song request.
GET /session-stats-This stream's live follower/sub/bits/message/viewer counts.
fetch('https://wastedlightbot.com/api/streamdeck/queue/open', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer wlb_yourkeyhere' },
});

Building your own plugin from scratch

This is the actual, complete source for the official plugin -- copy it as a starting point, add your own buttons to it, and package it yourself. Requires Node.js installed.

1. Set up the project folder. Elgato plugins need a specific folder structure -- the outer folder holds your source, and a .sdPlugin folder inside it is what actually gets installed.

mkdir my-wastedlightbot-plugin cd my-wastedlightbot-plugin npm init -y npm install @elgato/streamdeck npm install --save-dev @elgato/cli esbuild mkdir -p com.yourname.streamdeck.sdPlugin/bin mkdir -p com.yourname.streamdeck.sdPlugin/ui mkdir -p com.yourname.streamdeck.sdPlugin/imgs

2. Create the manifest at com.yourname.streamdeck.sdPlugin/manifest.json -- this describes every button your plugin provides. Add a new entry to the Actions array for each button you want:

{ "Name": "WastedLightBot", "UUID": "com.wastedlightbot.streamdeck", "Version": "1.0.0.0", "Author": "WastedLightBot", "Description": "Trigger WastedLightBot actions -- pick a giveaway winner, skip the song queue, and more -- straight from your Stream Deck.", "Category": "WastedLightBot", "Icon": "imgs/plugin-icon", "CodePath": "bin/plugin.js", "Nodejs": { "Version": "20", "Debug": "disabled" }, "SDKVersion": 2, "Software": { "MinimumVersion": "6.5" }, "OS": [ { "Platform": "windows", "MinimumVersion": "10" }, { "Platform": "mac", "MinimumVersion": "12" } ], "Actions": [ { "Name": "Pick Giveaway Winner", "UUID": "com.wastedlightbot.streamdeck.pickwinner", "Icon": "imgs/action-giveaway", "Tooltip": "Picks a random winner from the current giveaway and ends it.", "PropertyInspectorPath": "ui/settings.html", "States": [ { "Image": "imgs/action-giveaway", "TitleAlignment": "bottom" } ] }, { "Name": "Skip Song", "UUID": "com.wastedlightbot.streamdeck.skipsong", "Icon": "imgs/action-skipsong", "Tooltip": "Skips whatever's currently playing in the song request queue.", "PropertyInspectorPath": "ui/settings.html", "States": [ { "Image": "imgs/action-skipsong", "TitleAlignment": "bottom" } ] }, { "Name": "Queue Next", "UUID": "com.wastedlightbot.streamdeck.queuenext", "Icon": "imgs/action-queue", "Tooltip": "Pulls the next person from the viewer queue.", "PropertyInspectorPath": "ui/settings.html", "States": [ { "Image": "imgs/action-queue", "TitleAlignment": "bottom" } ] } ] }

3. Create the plugin logic at src/plugin.js (this gets bundled into bin/plugin.js later, not edited directly there). Add a new entry to ACTION_ENDPOINTS for each new button -- the key is the action's UUID from your manifest, the value is whichever endpoint from the table above it should call:

const streamDeck = require('@elgato/streamdeck').default; const API_BASE = 'https://wastedlightbot.com/api/streamdeck'; // UUID -> which endpoint it hits. Every action shares the same API key // (stored in global settings, entered once via any button's settings UI), // so a single map of "button pressed -> endpoint to call" is all we need -- // no per-action classes required for something this simple. const ACTION_ENDPOINTS = { 'com.wastedlightbot.streamdeck.pickwinner': '/giveaway/pick-winner', 'com.wastedlightbot.streamdeck.skipsong': '/song/skip', 'com.wastedlightbot.streamdeck.queuenext': '/queue/next', }; async function callWastedLightBot(endpoint) { const settings = await streamDeck.settings.getGlobalSettings(); const apiKey = settings.apiKey; if (!apiKey) { return { ok: false, error: 'No API key set -- open this button\'s settings and paste your key from the dashboard.' }; } try { const res = await fetch(`${API_BASE}${endpoint}`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, }); const data = await res.json(); if (!res.ok) return { ok: false, error: data.error || `Request failed (${res.status})` }; return { ok: true, data }; } catch (err) { return { ok: false, error: `Couldn't reach WastedLightBot: ${err.message}` }; } } streamDeck.actions.onKeyDown(async (ev) => { const endpoint = ACTION_ENDPOINTS[ev.action.manifestId]; if (!endpoint) return; // not one of ours, ignore const result = await callWastedLightBot(endpoint); if (!result.ok) { streamDeck.logger.error(`[${ev.action.manifestId}] ${result.error}`); await ev.action.showAlert(); // Stream Deck's built-in red-flash error indicator return; } await ev.action.showOk(); // Stream Deck's built-in green-checkmark success indicator }); streamDeck.connect();

4. Create the settings page at com.yourname.streamdeck.sdPlugin/ui/settings.html -- this is what shows up when someone configures a button, letting them paste their API key:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> body { font-family: 'Segoe UI', sans-serif; background: #2D2D2D; color: #EFEFEF; margin: 0; padding: 16px; font-size: 13px; } label { display: block; margin-bottom: 6px; color: #A8A8A8; } input { width: 100%; box-sizing: border-box; background: #1E1E1E; border: 1px solid #444; color: #EFEFEF; padding: 8px; border-radius: 4px; font-family: monospace; } .hint { margin-top: 8px; color: #888; line-height: 1.5; } .hint a { color: #6BB8F0; } #saved { color: #4ADE80; margin-left: 8px; display: none; } </style> </head> <body> <label for="apiKey">WastedLightBot API key</label> <input type="text" id="apiKey" placeholder="wlb_..."> <span id="saved">Saved ✓</span> <div class="hint"> Get this from your dashboard's Bot tab, under "Stream Deck." This is shared across all WastedLightBot buttons -- you only need to enter it once, on any one of them. </div> <script> // Standard Stream Deck property inspector registration -- Stream Deck itself // calls this function once the page loads, providing the WebSocket port and // registration info needed to talk back to the plugin. let websocket = null; let pluginUUID = null; // shared between the connect callback and the input listener below window.connectElgatoStreamDeckSocket = (port, uuid, registerEvent) => { pluginUUID = uuid; websocket = new WebSocket(`ws://127.0.0.1:${port}`); websocket.onopen = () => { websocket.send(JSON.stringify({ event: registerEvent, uuid })); // Global settings are shared across every button this plugin provides, // so entering the key once on any button carries over to the rest. websocket.send(JSON.stringify({ event: 'getGlobalSettings', context: uuid })); }; websocket.onmessage = (msg) => { const data = JSON.parse(msg.data); if (data.event === 'didReceiveGlobalSettings') { const settings = data.payload.settings || {}; if (settings.apiKey) document.getElementById('apiKey').value = settings.apiKey; } }; }; document.getElementById('apiKey').addEventListener('input', (e) => { if (!websocket || websocket.readyState !== WebSocket.OPEN) return; websocket.send(JSON.stringify({ event: 'setGlobalSettings', context: pluginUUID, payload: { apiKey: e.target.value.trim() }, })); const saved = document.getElementById('saved'); saved.style.display = 'inline'; clearTimeout(window._savedTimeout); window._savedTimeout = setTimeout(() => saved.style.display = 'none', 1200); }); </script> </body> </html>

5. Bundle and package it. The plugin needs its dependency bundled into a single file before packaging -- Stream Deck has no way to run npm install on someone else's machine:

npx esbuild src/plugin.js --bundle --platform=node --outfile=com.yourname.streamdeck.sdPlugin/bin/plugin.js npx streamdeck validate com.yourname.streamdeck.sdPlugin npx streamdeck pack com.yourname.streamdeck.sdPlugin

That last command produces a .streamDeckPlugin file -- double-click it to install your custom version into Stream Deck.