Build a Slack bot on Hetzner inference
Verified August 12, 2026
Hetzner runs open models on its own hardware and serves them over an OpenAI-compatible API. It is experimental and, while it stays that way, free.
That is enough to run a Slack bot on. The engine calls the endpoint, so the only thing that changes from any other provider is two lines of config: the type and the base URL.
This takes about five minutes.
What you need
- A Slack workspace where you can install apps
- A Hetzner Experiments account and an API token
Install the CLI
npm i -g @substructure.ai/cliWrite the config
Save this as substructure.toml.
name = "slack-bot"
[llm.hetzner]
type = "openai"
base_url = "https://inference.hetzner.com/api/v1"
[agent.assistant]
llm = "hetzner"
model = "DeepSeek-V4-Flash-0731"
system = "You are a helpful assistant. Reply using Slack mrkdwn."
[slack]
dm = "assistant"
mentions = "assistant"
[remote]
url = "https://api.substructure.ai"type = "openai" says the engine makes the call in the OpenAI Chat Completions
shape. base_url says where. Hetzner answers that shape, so nothing else in the
file knows the difference.
[agent.assistant] declares the agent: its model, its prompt, and what it can
reach. The Slack bot is how you talk to it.
[remote] is the deployment the CLI talks to. subs apply writes the org and
the project back under it.
Create the project
subs applyThis creates the project from the file and writes the project id back into it.
If you are not signed in yet, the command opens your browser first. Run
subs apply again after any edit to the file.
Add your Hetzner token
subs llm set-key hetznerThe name is the block's, not the vendor's. The command reads the token from stdin and stores it with your deployment. It never goes in the config file.
Create the token in Hetzner Experiments. Check it before you go further:
curl -s https://inference.hetzner.com/api/v1/models \
-H "Authorization: Bearer $HETZNER_TOKEN"That is also the live model list. The set below is what it served on the day this was written, and an experimental platform adds to it.
Running the engine on your own machine instead? Add api_key_env = "HETZNER_TOKEN"
to the block and the local engine reads the token from that variable. subs apply
strips the line, and a deployment refuses a document that still carries one.
Connect Slack
subs slack connectThis opens Slack's consent page. Pick your workspace and approve. The bot is live once the command prints the workspace name.
Use it
Invite the bot to a channel and mention it. It replies in the thread. Later mentions in that thread continue the same conversation. DM it and it replies to every message.
Which model to run
Every model here is open weights, and every one is a mixture of experts, so the active parameter count is what it costs to answer, not the total.
| Model | Active / total | Context (tokens) | Reads images | License |
|---|---|---|---|---|
DeepSeek-V4-Flash-0731 | 13B / 304B | 512,000 | No | MIT |
GLM-5.2-NVFP4 | 40B / 744B | 512,000 | No | MIT |
Kimi-K2.7-Code | 32B / 1T | 262,144 | Yes | Modified MIT |
Qwen/Qwen3.6-35B-A3B-FP8 | 3B / 35B | 262,144 | Yes | Apache 2.0 |
A model that reads images can answer about a screenshot someone pastes into the thread. Context is what caps a thread: every turn resends it, so start a new thread for a new topic.
GLM-5.2-NVFP4 is listed and served nothing on 2026-08-12. Every request timed
out at the gateway, with or without tools, streaming or not, down to a
sixteen-token hello. It is an experimental platform and this is what that means.
Run the curl above before you point an agent at a model, and treat the set as
something that moves.
Change the model line to any id from the table and run subs apply. The rest
of the file stays the same.
[agent.assistant]
model = "Qwen/Qwen3.6-35B-A3B-FP8"Hetzner provides the models as their developers publish them, under the licenses above. Using the API means accepting them.
What it costs and what it limits
Free while the Inference API is experimental. Hetzner says it will email before that changes.
The limits are on throughput, per API key:
| Window | Input tokens | Output tokens |
|---|---|---|
| 60s | 10M | 200k |
Over either and the API answers 429. A Slack bot is nowhere near this on its
own — a busy thread is a few thousand tokens a turn — so a 429 usually means
something else on the same token is running a batch.
Hetzner stores what it needs to meter usage: timestamps and token counts, not the content of requests or responses.
Pin it to one channel
mentions answers anywhere the bot is invited. Name a channel to give it its own
agent, or to leave mentions off and turn the channel table into an allowlist.
[slack.channel.C0BQH6E4RGQ]
agent = "assistant"Name a channel by id, never by #name. The id is in the channel's About tab and
at the end of its link.
Give it tools
An agent can call MCP servers. Add one and name it on the agent.
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"
[agent.assistant]
mcp = ["sentry"]subs mcp login sentry
subs applyThe engine authorizes the connection, fetches its tools, and runs every call. Your code never holds a token.
Hetzner's docs do not mention tool calling, but the endpoint serves it. Checked
against all four models on 2026-08-12: DeepSeek-V4-Flash-0731,
Kimi-K2.7-Code, and Qwen/Qwen3.6-35B-A3B-FP8 each answered a tool definition
with a tool_calls message, took the tool result back, and finished the turn on
it. Two calls in one message work, so an agent can hit two connections at once.
Tool calls stream. Names carrying the connection prefix, such as
sentry__search_issues, come back intact, which is the form the engine gives
MCP tools.
If it does not answer
- Bad token.
curlthe models endpoint.401means the token, not the bot. - Wrong base URL. It is
https://inference.hetzner.com/api/v1and the engine appends the rest. Dropping/apigives a404on every call. - No API key. Run
subs llm set-key hetzner. There is no platform key to fall back to. 429under load. The per-key window is 60 seconds. Wait it out or split the traffic across tokens.504, or nothing at all. A listed model is not always a served one. Send the same prompt withcurl. If that hangs too, the model is down and the fix is anothermodelline, not the config.- No
[slack]section. A connected workspace with no[slack]never replies. Adddmandmentions. - Bot not invited. Invite it to the channel before you mention it.
- Changes not applied. Run
subs applyafter every edit to the file. - Anything else. Run
subs doctorto see what the project still needs.
Next steps
- Run the bot on an open model through OpenRouter: the wider set, with prices and what a thread costs.
- Connect the bot to Sentry so it can look up errors, stack traces, and releases in your Sentry org.
- How the Slack integration works: threads, DMs, and per-channel agents.
- How the engine calls your model:
base_url, where the key lives, and calling the model yourself. - Pricing: one flat price a month, and no per-token charges from us.