OpenClaw’s Architecture — A Short Breakdown

2026/02/15

BUILDai-tooling

I set up OpenClaw this week — it’s an open-source gateway that lets you talk to LLMs through Telegram, Discord, Slack, whatever. Sounds simple. But the architecture has a surprising number of moving parts, and setting it up forced me to think clearly about things like WebSockets, loopback binding, heartbeats, and layered auth.

Here’s the breakdown.

The five parties

There are five things in the chain, and each pair has its own auth boundary:

You (phone) → Telegram (cloud) → Gateway (your Mac) → LLM (Anthropic/OpenAI)
                                       ↓
                                  Tools/Nodes (shell, browser, etc.)

That’s it. The gateway is the brain — it sits on your machine, receives messages from channels, forwards them to an LLM, and sends responses back. Everything flows through it.

Five auth hops

Each arrow in that diagram is a separate trust boundary with its own credentials:

Hop Who → Who How
1 You → Telegram Your Telegram account
2 Telegram → Gateway Bot token (gateway authenticates as the bot)
3 You → Gateway Pairing — the gateway asks “do I know this person?”
4 Gateway → LLM API key (OpenAI, Anthropic, etc.)
5 Gateway → Tools Allowlists + sandboxing

Hop 3 is the interesting one. When an unknown person DMs your bot, the gateway doesn’t just let them through. It generates a one-time pairing code, and you — the owner — have to approve it from the CLI:

openclaw pairing approve telegram TDGZS55R

Until you do, the bot ignores their messages entirely. It’s human-in-the-loop access control before any LLM call happens. That approval gets persisted to an allowlist file on disk.

The trust hierarchy is: Owner (full trust) → Allowlisted contacts (pairing-verified) → Strangers (blocked by default).

“Authenticates as the bot” — what does that actually mean?

Hop 2 confused me at first. “The gateway authenticates as the bot.” But… isn’t the bot a program? Where does it run?

Here’s the thing: the bot isn’t a running program. It’s a row in Telegram’s database — a name, an ID, and a token. When you create a bot via @BotFather, Telegram gives you a token like 8037632784:AAHtjTJVoFJCfiL1rd7_.... That token is the bot’s entire identity.

Your gateway uses that token to say to Telegram: “I am bot 8037632784, hand me my messages.” Telegram checks the token, looks up the bot, and complies. The gateway is wearing the bot’s identity. The bot itself never “runs” anywhere — it’s just a credential that your gateway presents.

Think of it like a mailbox key. The mailbox (bot) sits on Telegram’s servers. The key (token) lets whoever holds it open the mailbox and read the mail.

How Telegram and the gateway actually connect

So if the bot isn’t a running program, and the gateway is on your Mac behind a loopback-only firewall… how do messages flow?

The gateway reaches out to Telegram. Not the other way around.

Polling — the gateway calls Telegram’s API in a loop:

Gateway ──HTTP GET──▶ api.telegram.org
         "any new messages for bot 8037632784?"
         (token in the request header)

Telegram ◀── "yes, here are 3 messages"

         ... a second later ...

Gateway ──HTTP GET──▶ "anything new?"
Telegram ◀── "nope"

         ... repeat forever ...

That’s it. The gateway calls https://api.telegram.org/bot<token>/getUpdates over and over. Telegram is passive — it just answers when asked.

There’s an alternative called webhooks, where you tell Telegram “POST to this URL whenever someone messages my bot.” But that requires your gateway to be publicly reachable on the internet. Ours is bound to 127.0.0.1 — nothing external can reach it. So polling is the way.

This is a useful mental model: the gateway initiates every connection. It reaches out to Telegram (polling). It reaches out to OpenAI (API calls). It reaches out to nothing that doesn’t have a public endpoint. The gateway is a client of everything, a server to nothing external.

WebSockets, not HTTP

The TUI connects to the gateway via ws://127.0.0.1:18789. Three things worth unpacking here.

WebSocket (ws://) — HTTP is request-response. You ask, the server answers, the connection closes. WebSocket is different: you open a connection and it stays open. Both sides can send data whenever they want. That’s what makes the TUI feel live — it’s not polling the gateway every second, it’s holding a persistent pipe.

Loopback (127.0.0.1) — This address means “myself.” Traffic on 127.0.0.1 never hits the network. It goes out the TCP stack and comes right back in. The gateway binds to loopback by default, which means only programs on your machine can talk to it. Someone on your WiFi can’t connect. That’s a security boundary, not just a convenience.

Port (18789) — Your Mac has one IP but runs hundreds of services. Ports distinguish them. The gateway listens on 18789; your browser uses 80/443; SSH uses 22. It’s just an address within an address.

Heartbeats and tick timeouts

WebSocket connections are long-lived, but networks don’t guarantee they stay alive. Routers, NAT devices, and OS kernels all kill idle connections after some time. So both sides send tiny ping/pong messages — heartbeats:

TUI ──ping──▶ Gateway     "still there?"
TUI ◀──pong── Gateway     "yep"
    ... 30 seconds ...
TUI ──ping──▶ Gateway     "still there?"
         ✗ no response
TUI: "tick timeout → disconnected"

“Tick timeout” means the gateway didn’t respond to a heartbeat in time. Usually because the gateway restarted, or the connection went genuinely stale. The TUI reconnects automatically.

Agents and sessions

The gateway organizes conversations into agents and sessions. An agent is a workspace with its own model config, tools, and system prompt. A session is one conversation thread within that agent.

Gateway
  └── Agent: main
        ├── Session: telegram-user-123
        ├── Session: tui-session
        └── Session: discord-thread-456

Messages from Telegram and the TUI can route to different agents (or the same one) based on routing rules. By default, everything goes to the main agent.

The config file

Everything lives in ~/.openclaw/openclaw.json. It’s surprisingly readable:

{
  "env": { "OPENAI_API_KEY": "sk-..." },
  "agents": {
    "defaults": {
      "model": { "primary": "openai/gpt-4o" }
    }
  },
  "channels": {
    "telegram": {
      "enabled": true,
      "dmPolicy": "pairing",
      "botToken": "..."
    }
  },
  "gateway": {
    "port": 18789,
    "bind": "loopback",
    "auth": { "mode": "token", "token": "..." }
  }
}

Model provider credentials, channel tokens, gateway auth, routing — it’s all in one file. File permissions are 600 (owner read/write only), which matters because this file holds every secret the system uses.

The mental model

If I had to explain OpenClaw in one sentence: it’s a local multiplexer that sits between your messaging apps and your LLM provider, with layered auth at every boundary.

The gateway is the single point of control. Channels are inputs. The LLM is the backend. Pairing is the access gate. And everything runs on your machine, bound to loopback, with credentials locked down to 600 permissions.

It’s more infrastructure than you’d expect for a chat bot. But that’s because it’s not really a chat bot — it’s an agent runtime with messaging as the interface.