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/cli

Write the config

Save this as substructure.toml.

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 apply

This 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 hetzner

The 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 connect

This 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.

ModelActive / totalContext (tokens)Reads imagesLicense
DeepSeek-V4-Flash-073113B / 304B512,000NoMIT
GLM-5.2-NVFP440B / 744B512,000NoMIT
Kimi-K2.7-Code32B / 1T262,144YesModified MIT
Qwen/Qwen3.6-35B-A3B-FP83B / 35B262,144YesApache 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.

substructure.toml
[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:

WindowInput tokensOutput tokens
60s10M200k

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.

substructure.toml
[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.

substructure.toml
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"

[agent.assistant]
mcp = ["sentry"]
subs mcp login sentry
subs apply

The 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. curl the models endpoint. 401 means the token, not the bot.
  • Wrong base URL. It is https://inference.hetzner.com/api/v1 and the engine appends the rest. Dropping /api gives a 404 on every call.
  • No API key. Run subs llm set-key hetzner. There is no platform key to fall back to.
  • 429 under 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 with curl. If that hangs too, the model is down and the fix is another model line, not the config.
  • No [slack] section. A connected workspace with no [slack] never replies. Add dm and mentions.
  • Bot not invited. Invite it to the channel before you mention it.
  • Changes not applied. Run subs apply after every edit to the file.
  • Anything else. Run subs doctor to see what the project still needs.

Next steps