Mallard: a Discord bot you write yourself
beta: pricing and limits still being tuned
What Easy Mode writes for you
Blocks compile to ordinary JavaScript, and you can read it below the canvas while you build. That file is the only thing that ever runs, so a script assembled here gets the same triggers and the same logs as one you typed. When you want the rest of the language, take the file and carry on in it.
- User id
- {{actorId}}
- Role id
- Verified
- Channel id
- #general
- Content
- Welcome! Read the rules and say hi.
compiles to
// Give a member a role
addRole(ctx.actorId, "1528016624483958906");
// Send a message
sendMessage("1527981654843457679", "Welcome! Read the rules and say hi.");30 action blocks, plus conditions and an early exit, up to
60 of them per script. It deliberately does not cover everything:
no loops, no stored data, no fetch, no buttons, and one embed at a time.
That is what the right-hand column above is for, and switching to Advanced hands you
the file to keep going in.
easy mode is an early preview, and still changing
Look inside the dashboard
Everything below is the actual editing surface, not a mockup.
Blocks, when you would rather not type it
Every block is one function from the same catalogue the docs list, filtered to what your trigger can call. The JavaScript it becomes sits under the canvas as you build.

A real editor, not a text box
Monaco with syntax highlighting and autocomplete against the same API the docs describe. Save and it is live, with no deploy step.

Slash commands you define
Typed options, required flags, and the Discord permission a member needs to see the command. Mallard registers it with Discord for you.

Storage your scripts share
A per-guild key-value store your scripts read and write, plus env values and write-only secrets, all editable from the dashboard.

What a script looks like
Plain JavaScript against a small API. Discord actions, an HTTP client, per-guild storage, and secrets your moderators never see.
Catch image-spam automatically
// Times out repeat image-posters for mods to review, no command needed
const key = `imgspam.${ctx.message.authorId}`;
const recent = JSON.parse(getKV(key) || "[]")
.filter(e => Date.now() - e.timestamp < 10000);
recent.push({ messageId: ctx.message.id, timestamp: Date.now() });
setKV(key, JSON.stringify(recent), 60);
if (recent.length >= 3 && ctx.message.attachments.length > 0) {
timeout(ctx.message.authorId, "Image spam", 1440);
sendMessage(ctx.env.mod_alert_channel_id, "🖼️ Timed out for image spam. Please review.");
}Call an AI without leaking your key
// ctx.secret.* are opaque {{secret:NAME}} tokens. The real key is
// injected only inside fetch(), never visible to the script (or your mods)
const chat = getMessages(ctx.channelId, 100).reverse()
.map(function (m) { return m.authorDisplayName + ": " + m.content; })
.join("\n");
const res = fetch("https://openrouter.ai/api/v1/chat/completions", JSON.stringify({
method: "POST",
headers: {
authorization: "Bearer " + ctx.secret.OpenrouterKey,
"content-type": "application/json"
},
body: JSON.stringify({
model: "google/gemini-3.1-flash-lite",
messages: [{ role: "user", content: "Summarize this chat:\n" + chat }]
})
}));
editResponse(JSON.parse(res.body).choices[0].message.content);Announce when you go live
// Twitch events arrive as triggers just like Discord ones
if (ctx.env.twitch_announce_channel_id) {
sendMessage(ctx.env.twitch_announce_channel_id, "", [{
title: "🔴 Live now: " + ctx.twitch.title,
description: "Playing " + ctx.twitch.category
+ " at twitch.tv/" + ctx.twitch.broadcasterLogin
}]);
}Start with what you already need
Install it, use the commands it comes with, and change them when they get in your way. Add the next one out of blocks, or write it. If you would rather see the whole API first, the docs cover every trigger and function.
Questions, or want to see what other servers are running? The support server has a scripts hub.