A Meld spark that plays a random clip from whichever channel you shout out — Kick or Twitch.
1 Copy the Prompt
This block gives Meld's AI everything it needs. Meld handles the chat commands; this prompt just provides the clip-fetching API the widget uses internally.
Build a shoutout clip overlay widget. When triggered, it plays one random clip from the shouted-out channel (Kick or Twitch), with a branded overlay on top (channel avatar, name, CTA), then cleans itself up.
## Data layer — included verbatim at the bottom of this message
Include the entire `<script>` block unchanged. It provides two global functions and bundles Hls.js.
### `window.fetchShoutoutClip(platform, channel)`
Returns a Promise that resolves to:
{
platform: "kick" | "twitch",
channel: "xqc",
channelAvatar: "https://...", // may be empty string
channelTitle: "xQc", // display name
clip: {
id: "string",
url: "https://...", // Kick = .m3u8, Twitch = .mp4
title: "clip title",
creatorName: "who clipped it",
viewCount: 12345,
durationMs: 28000
}
}
Rejects on error (channel not found, no clips, network fail).
### `window.loadShoutoutClip(videoEl, url)`
Plays a clip URL in a `<video>` element. Handles `.m3u8` (Kick, via Hls.js) and `.mp4` (Twitch, native) automatically. Pass `null` as the URL to tear down cleanly (clears src and destroys any Hls instance).
**Never set `videoEl.src` directly** — Kick clips won't play without Hls.js. Always go through `loadShoutoutClip`.
## Widget contract
The widget must listen to live Twitch/Kick chat itself and react to the user's configured commands. Use the Firebase Web SDK (Meld exposes chat messages through Firebase) — load it from the Firebase CDN and subscribe to the chat stream. When a message matches the user's Kick command, fire a Kick shoutout; when it matches the Twitch command, fire a Twitch shoutout.
The widget must ALSO expose two trigger paths so the same shoutout flow can be fired without chat (testing, dashboard buttons, etc.):
1. **Global functions:** `window.shoutoutKick(channel)` and `window.shoutoutTwitch(channel)`.
2. **Custom event:** `window.addEventListener('shoutout', e => ...)` where `e.detail = { platform, channel }`.
All three trigger paths (chat message, function call, event) must end up in the same place — the fetch/play pipeline. Simplest structure: the chat listener parses messages into `{ platform, channel }`, then calls the same handler the global function / event listener call. One pipeline, three entry points.
After setup, the widget should log the two command names to the console (e.g. `Shoutout widget active — !sok for Kick, !sot for Twitch`) so the user can verify everything wired up correctly.
## Behavior
- Idle state: fully hidden (transparent background, for OBS browser source).
- On trigger: `await fetchShoutoutClip(platform, channel)` → `loadShoutoutClip(videoEl, url)` → play → show overlay with entrance animation.
- On `video.ended`: run exit animation, then hide the widget and tear down with `loadShoutoutClip(videoEl, null)`.
- If `fetchShoutoutClip` rejects: log to console, stay hidden, don't crash.
- If a trigger arrives while a shoutout is playing: queue it. Don't interrupt the current clip.
- Long channel names and titles should truncate gracefully (ellipsis or marquee).
## Required inputs — ask the user for these before building
- **Kick chat command** — what they type in chat to shout someone out from Kick. Default: `!sok`. Examples: `!sok`, `!shoutoutkick`, `!kickso`, `!plug`.
- **Twitch chat command** — same but for Twitch. Default: `!sot`. Examples: `!sot`, `!shoutouttwitch`, `!twitchso`, `!raid`.
Validate: must start with `!`, contain only letters/numbers/underscores after the `!`, and the two commands must be different from each other. If the user enters an invalid or duplicate command, re-ask.
Both commands take one argument: the channel name being shouted out. Usage in chat will be `<command> <channel>` — for example, if the user picks `!sok` for Kick, they'll type `!sok xqc` in chat to trigger a Kick shoutout for xqc.
## Styling — offer the user these choices before building
- **Vibe:** hype/announcement, friendly/cozy, gaming HUD, arcade marquee, cinematic title card, news alert, trading card reveal, esports broadcast, neon sign
- **Overlay content:** any combination of channel avatar, channel display name, call-to-action line ("Go follow @X"), clip title, clip creator, or nothing at all
- **Entrance animation:** slide in, zoom bounce, type-on text, glitch reveal, iris wipe
- **Exit animation:** fade, slide out, glitch, hard cut
- **Frame/border:** none, retro TV, neon outline, letterbox, polaroid, arcade cabinet
- **Fonts:** any Google Font
- **Volume:** default 100%, user-overridable
## Technical constraints
- Vanilla JS and CSS, no frameworks.
- Transparent background.
- The data layer block must be included verbatim.
- No hardcoded channel names — the channel arrives via the trigger call.
## Data layer (include verbatim)
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
(function() {
const ENDPOINTS = {
kick: 'https://mrboostlive.com/kick/clips/clips.php',
twitch: 'https://mrboostlive.com/twitch/clips/clip-data.php'
};
// Normalize Kick's response shape into the unified shape
function normalizeKick(data, channel) {
return {
platform: 'kick',
channel,
channelAvatar: data.channel?.avatar || '',
channelTitle: data.channel?.display_name || data.channel?.name || channel,
clip: {
id: data.id,
url: data.clip_url, // .m3u8 HLS — widget must use Hls.js
title: data.title || '',
creatorName: data.creator?.username || '',
viewCount: data.view_count || 0,
durationMs: (data.duration || 0) * 1000
}
};
}
// Normalize Twitch's response shape into the unified shape
function normalizeTwitch(data, channel) {
const clip = data.clip || {};
return {
platform: 'twitch',
channel,
channelAvatar: data.channel?.avatar || '',
channelTitle: data.channel?.display_name || data.channel?.login || channel,
clip: {
id: clip.id,
url: clip.video_url, // signed .mp4 — plays natively
title: clip.title || '',
creatorName: clip.creator_name || '',
viewCount: clip.view_count || 0,
durationMs: (clip.duration || 0) * 1000
}
};
}
window.fetchShoutoutClip = async function(platform, channel) {
if (!channel || typeof channel !== 'string') throw new Error('Missing channel name');
const p = String(platform).toLowerCase();
if (!ENDPOINTS[p]) throw new Error(`Unknown platform: ${platform}`);
// Kick's clip endpoint returns avatar: null for most channels, so grab
// the user profile in parallel and merge it in after normalization.
const clipFetch = fetch(`${ENDPOINTS[p]}?channel=${encodeURIComponent(channel)}`);
const userFetch = p === 'kick'
? fetch(`https://mrboostlive.com/kick/api/?channel=${encodeURIComponent(channel)}`).catch(() => null)
: null;
const res = await clipFetch;
if (!res.ok) throw new Error(`${p} lookup failed (${res.status})`);
const data = await res.json();
const normalized = p === 'twitch' ? normalizeTwitch(data, channel) : normalizeKick(data, channel);
if (p === 'kick' && !normalized.channelAvatar && userFetch) {
try {
const userRes = await userFetch;
if (userRes && userRes.ok) {
const userData = await userRes.json();
if (userData?.user?.profile_pic) normalized.channelAvatar = userData.user.profile_pic;
}
} catch (_) { /* non-fatal — just use empty avatar */ }
}
if (!normalized.clip.url) throw new Error(`No playback URL for ${channel}`);
return normalized;
};
// Helper for the widget — loads a clip URL into a video element,
// transparently handling both .m3u8 (Kick, needs Hls.js) and .mp4 (Twitch, native).
// Use this in your "ended"/"error" cleanup: call loadShoutoutClip(videoEl, null) to tear down.
window.loadShoutoutClip = function(videoEl, url) {
if (videoEl._hls) { try { videoEl._hls.destroy(); } catch (_) {} videoEl._hls = null; }
if (!url) { videoEl.removeAttribute('src'); videoEl.load(); return; }
if (url.includes('.m3u8') && window.Hls && Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(videoEl);
videoEl._hls = hls;
} else {
videoEl.src = url;
}
};
})();
</script>
2 Quick Start
In Meld, create a new spark.
Paste the block above into the spark prompt.
Answer the styling questions — vibe, overlay content, animations, test channel.
Once the widget is built, use Meld's chat-command UI to wire a chat trigger (like !sok <name> or !sot <name>) to call window.shoutoutKick(name) or window.shoutoutTwitch(name).
Name the chat commands whatever fits your chat — the widget doesn't care, it just reacts to the function call.