Image generated by ChatGPT - it dropped the ball at the end of the speech bubble.
A simple Discord bot that watches one channel and replies with a Pig Latin translation of any human message that directly @mentions it.
Runs as an ordinary *nix program, and on BareMetal - an exokernel, written in x86-64 Assembly, built to run a single, statically-linked program directly on hardware (or a microVM) with no OS overhead in between.
Why this exists
Most Discord bots likely assume a full OS, a language runtime, a garbage collector, and hundreds of megabytes of headroom for good measure. This one doesn't. Every buffer in discord_bot.c is static and of fixed-size - nothing is malloc'd - specifically so it can run in a RAM-constrained environment.
In a world where "small" usually still means a few hundred MiB, this bot has been tested running successfully end-to-end, polling Discord and posting replies, inside a 4MiB Firecracker microVM on BareMetal Cloud:
And here it is replying in the wild, in the Return Infinity Discord #testing channel:
How it works
- Polling, not the Gateway. A "real" Discord bot normally holds a
persistent WebSocket open to Discord's Gateway and gets pushed
MESSAGE_CREATEevents. That's not available here - the BareMetal-Appcurlport this is built against has WebSockets compiled out (HTTP/HTTPS only). So instead this pollsGET /channels/{id}/messageseveryPOLL_INTERVAL_SECS, usingafterto only ask for messages newer than the last one it already handled, and replies viaPOST /channels/{id}/messages- both ordinary HTTPS requests through libcurl.
@mentiongating. On startup the bot looks up its own user id viaGET /users/@me, then only replies to a message whose raw content contains a literal<@id>or<@!id>for that id. Everything else (@everyone, role mentions, plain messages) is ignored.- Hand-rolled JSON. There's no JSON library here -
json_extract_string()/json_extract_object()/split_json_objects()do just enough parsing for this shape of Discord response, tracking string/escape state and brace/bracket depth so nested objects (like a reply'sreferenced_message, or a mentioned user's own"id") can't be mistaken for the outer message's fields. - Pig Latin rules. A leading vowel run gets
"way"appended; otherwise the leading consonant cluster (a trailing"qu"counts as part of it, so "queen" -> "eenquay", not "ueenqay") moves to the end plus"ay"; a word with no vowels just gets"ay". Only the first letter's case survives (ALL-CAPS input comes back Titlecase). Discord mentions/channels/emoji (<@id>,<#id>,<:name:id>) andhttp(s)://URLs are passed through untouched so translating them can't break a real ping or a working link.
Build instructions
BareMetal
git clone https://github.com/ReturnInfinity/BareMetal-App cp discord_bot.c BareMetal-App/ cd BareMetal-App ./setup.sh ./1-build.sh discord_bot.c ./2-run.sh ./3-upload.sh # optional - upload to BareMetal Cloud
*nix (Linux/BSD/macOS)
discord_bot.c is also a valid standalone C program - just link it against libcurl:
gcc -O2 -o discord_bot discord_bot.c -lcurl ./discord_bot
Discord setup
- Create an application + bot at the Discord Developer Portal.
- Copy the bot token into
DISCORD_BOT_TOKENindiscord_bot.c. - On the Bot tab, enable the Message Content Intent toggle - without it, Discord redacts the
contentfield (returns"") on every message this bot didn't send itself, for both the Gateway and this REST endpoint. - Invite the bot to a server with the
botscope and the Read Messages/View Channel + Send Messages permissions. - With Developer Mode enabled in Discord, right-click the target channel -> Copy Channel ID -> set
DISCORD_CHANNEL_IDindiscord_bot.c.
main() refuses to run with either value left empty. Both are per-bot secrets, so don't commit real values into a shared/example file.
Notes
- TLS verification is off.
CURLOPT_SSL_VERIFYPEER/VERIFYHOSTare both disabled since no CA store is vendored on the BareMetal-Appcurlport. TLS still buys confidentiality on the wire, just not server-identity assurance - acceptable for hitting one known-good public endpoint, but the bot token is a bearer credential regardless, so treat it like a password. - No message loss across polls, only latency.
afteralways advances to the newest message id seen, so a burst larger thanMAX_MSG_OBJSjust finishes over the next poll or two rather than dropping anything. - Memory footprint. All buffers are static and sized generously relative
to Discord's own limits (e.g.
CONTENT_MAXleaves headroom under Discord's 2000-character cap) while still fitting comfortably inside a 4MiB microVM.
License
MIT - see LICENSE.


