# ๐Ÿง  Setting Up OpenCode Agent: Self-Hosted AI Coding Agent (Headless Server + Telegram Bot)

**Date:** July 16, 2026  
**Tags:** AI, OpenCode, Coding Agent, Self-Hosted, Telegram, 9Router, MCP, AI Agents

---

## ๐ŸŽฏ Overview

[OpenCode](https://github.com/opencode-ai/opencode) is a terminal-native **AI coding agent** โ€” like [Pi](/blog/pi-coding-agent-setup/) it edits files and runs commands, but OpenCode adds a **headless server mode** (`opencode serve`) and a dedicated **Telegram bot**, so you can drive coding sessions from chat without a terminal open. It's the *always-reachable coder* of the stack.

OpenCode is model-agnostic and here it rides **9Router** (and an OpenRouter fallback) for free inference. It also speaks **MCP**, so we wire in **GBrain** (shared memory) like the other agents.

### Why OpenCode?

| Feature | Benefit |
|---------|---------|
| **๐Ÿ–ฅ๏ธ CLI + Headless** | Interactive TUI *and* `opencode serve` HTTP server |
| **๐Ÿ“ก Telegram Bot** | Code from chat, no terminal required |
| **๐Ÿ”Œ MCP Ready** | GBrain + any MCP server |
| **๐ŸŒ 9Router / OpenRouter** | Free models with automatic fallback |
| **๐Ÿ’พ Session Store** | SQLite-backed sessions (with a hardening caveat โ€” see below) |
| **๐Ÿ”ง Agents** | Named agents (e.g. `build`) + per-project models |

---

## ๐Ÿ—๏ธ Architecture Overview

```
   Telegram โ”€โ”€โ–บ opencode-telegram-bot โ”€โ”€โ”
                                   โ”‚      โ”‚
   Browser/TUI โ”€โ”€โ–บ opencode (CLI) โ”€โ”ค      โ–ผ
                       โ”‚            โ”œโ”€โ”€โ–บ opencode serve (headless HTTP, :)
                       โ”‚            โ”‚        โ”‚
                       โ”‚            โ”‚        โ–ผ
                       โ”‚            โ”‚   9Router / OpenRouter (free models)
                       โ”‚            โ”‚        โ”‚
                       โ””โ”€โ”€ MCP โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ–บ GBrain (shared memory / Postgres)
```

---

## ๐Ÿ“‹ Prerequisites

- Linux VM with Node toolchain (OpenCode ships as a Node package).
- A model backend โ€” **9Router** (see the [9Router guide](/blog/9router-setup/)) and/or OpenRouter.
- (Optional) GBrain MCP server for shared memory.
- A Telegram bot token from [@BotFather](https://t.me/BotFather) (placeholder below).
- Non-root user (`ryan` in examples).

> โš ๏ธ **Security note:** Every secret below is a ``. Never commit real keys.

---

## ๐Ÿ“ฆ Step 1 โ€” Install OpenCode

```bash
# Install OpenCode globally
npm install -g opencode-ai

# On this VM the binary resolves here:
# /home/ryan/.hermes/node/bin/opencode ->
#   ../lib/node_modules/opencode-ai/bin/opencode.exe
# A local copy also lives at /home/ryan/.opencode/bin/opencode

# Verify
which opencode
opencode --help
```

---

## โš™๏ธ Step 2 โ€” Model Backend (9Router + OpenRouter)

OpenCode reads its config from `~/.config/opencode/opencode.jsonc`. The model backend is wired through **9Router** (primary) with an **OpenRouter** fallback. A small helper fetches the 9Router key at runtime:

```python
# ~/.codex/get-9router-key.py (sanitized concept)
# Reads the LAN 9Router API key from a local secret file and prints it.
import pathlib
secret = pathlib.Path("/home/ryan/.hermes/scripts/9router.txt").read_text().strip()
print(secret)  # use as  โ€” never hardcode
```

```toml
# ~/.codex/config.toml (sibling agent; same pattern OpenCode uses)
[mcp_servers.gbrain]
command = "/home/ryan/.bun/bin/gbrain"
args = ["serve"]

[mcp_servers.gbrain.env]
GBRAIN_DATABASE_URL = ""
GBRAIN_DIRECT_DATABASE_URL = ""
PATH = "/home/ryan/.bun/bin:/home/ryan/.local/bin:/usr/local/bin:/usr/bin:/bin"

[projects."/home/ryan/.codex"]
trust_level = "trusted"

[sandbox_workspace_write]
writable_roots = ["/home/ryan"]
network_access = true
```

OpenCode registers 9Router as a custom **OpenAI-compatible** provider in `~/.config/opencode/opencode.jsonc` (OpenCode v1.15.7 uses the `@ai-sdk/openai-compatible` package under the hood). Drop this block in and restart `opencode serve` + the Telegram bot:

```jsonc
// ~/.config/opencode/opencode.jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "9router": {
      "name": "9Router",
      "package": "@ai-sdk/openai-compatible",
      "url": "http://192.168.51.115:20128/v1",
      "apiKey": "",
      "models": [
        { "id": "Free", "name": "9Router Free (auto-rotating)" },
        { "id": "nvidia/z-ai/glm-5.2", "name": "GLM 5.2" },
        { "id": "nvidia/deepseek-ai/deepseek-v4-pro", "name": "DeepSeek V4 Pro" },
        { "id": "qwen/qwen3-max", "name": "Qwen3 Max" }
      ]
    }
  }
}
```

> ๐Ÿ’ก Key facts from the live setup:
> - `package` **must** be `@ai-sdk/openai-compatible` โ€” a bare `baseUrl`/`apiKey` block is silently ignored by OpenCode.
> - `url` points at the 9Router OpenAI-compatible endpoint (`/v1`). 9Router exposes **49 models** via `/v1/models`, and OpenCode auto-discovers them.
> - The `models` array is optional but pins the models you want in the picker (e.g. `Free` for the auto-rotating free tier).
> - After editing, reload config: `systemctl --user restart opencode-telegram-bot.service`.
> - In the Telegram bot, switch the model to **9Router / Free** to route through it.
> - Keep `` as a placeholder in this public article; the real LAN key is injected locally. The endpoint is LAN-only (`192.168.51.115`), not public.

---

## ๐Ÿงฉ Step 3 โ€” Wire GBrain via MCP

OpenCode supports MCP servers (same as the other agents). The GBrain server is invoked the same way:

```jsonc
// OpenCode MCP config (concept โ€” field names mirror the agent style)
{
  "mcpServers": {
    "gbrain": {
      "command": "/home/ryan/.bun/bin/gbrain",
      "args": ["serve"],
      "env": {
        "GBRAIN_DATABASE_URL": "",
        "GBRAIN_DIRECT_DATABASE_URL": ""
      }
    }
  }
}
```

Verify the server is healthy before relying on it:

```bash
/home/ryan/.bun/bin/gbrain get_health
# -> healthy
```

---

## ๐Ÿš€ Step 4 โ€” Headless Server Mode (`opencode serve`)

OpenCode can run as a **headless HTTP server** so other clients (and the Telegram bot) talk to it:

```bash
# Start the headless server on a fixed port, localhost only
opencode serve --port 8755 --hostname 127.0.0.1 --cors ""

# Flags:
#   --port        listen port (default 0 = random/ephemeral)
#   --hostname    bind host (default 127.0.0.1)
#   --cors        extra allowed CORS origins
#   --mdns        enable mDNS discovery (binds 0.0.0.0)
#   --pure        run without external plugins
#   --print-logs  stream logs to stderr
```

On this VM an `opencode serve` process runs persistently (PID seen via `pgrep -af 'opencode serve'`).

---

## ๐Ÿค– Step 5 โ€” Telegram Bot

OpenCode has a companion **Telegram bot** (`opencode-telegram-bot`) that bridges chat โ†’ coding sessions. Its settings live at `~/.config/opencode-telegram-bot/settings.json`.

```json
{
  "currentProject": {
    "id": "",
    "worktree": "/home/ryan/opencode/github/OpenCode",
    "name": "/home/ryan/opencode/github/OpenCode"
  },
  "currentAgent": "build",
  "currentModel": {
    "providerID": "opencode",
    "modelID": "big-pickle",
    "variant": "default"
  },
  "scheduledTasks": [],
  "sessionDirectoryCache": { "version": 1, "directories": [] }
}
```

Run the bot (token is a placeholder):

```bash
# Bot token from @BotFather โ€” DO NOT commit the real value
export TELEGRAM_BOT_TOKEN=""

# Launch the bot (concept)
opencode-telegram-bot --token "$TELEGRAM_BOT_TOKEN"

# Or as a managed service (see Step 6)
systemctl --user restart opencode-telegram-bot.service
```

> ๐Ÿ” The bot keeps a `pinnedMessageId` so it can update a live status message in the chat as the session progresses.

---

## ๐Ÿ› ๏ธ Step 6 โ€” systemd Service (Telegram Bot)

The Telegram bot runs as a managed **user service** so it survives reboots:

```ini
# ~/.config/systemd/user/opencode-telegram-bot.service
[Unit]
Description=OpenCode Telegram Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/ryan
ExecStart=/home/ryan/.opencode/bin/opencode-telegram-bot
Environment=TELEGRAM_BOT_TOKEN=
Environment=NINEROUTER_API_KEY=
Environment=GBRAIN_DATABASE_URL=
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
```

```bash
systemctl --user daemon-reload
systemctl --user enable --now opencode-telegram-bot.service
systemctl --user is-active opencode-telegram-bot.service
# -> active
```

---

## ๐Ÿ—„๏ธ Step 7 โ€” Session SQLite Hardening (Important!)

OpenCode stores sessions in **SQLite**. We hit a real bug: a session row created without an initial message violates a `NOT NULL` constraint on `session_message.seq`, which crashes the bot with:

```text
SQLiteError: NOT NULL constraint failed: session_message.seq
```

**Permanent fix** โ€” a SQLite trigger that seeds the first message, plus a cron watchdog that prunes empty sessions:

```sql
-- Trigger: ensure a session always has an initial seq=0 message
CREATE TRIGGER IF NOT EXISTS session_init_message
AFTER INSERT ON sessions
BEGIN
  INSERT INTO session_message (session_id, seq, role, content)
  SELECT NEW.id, 0, 'system', 'session initialized'
  WHERE NOT EXISTS (
    SELECT 1 FROM session_message WHERE session_id = NEW.id
  );
END;
```

```bash
# 5-minute watchdog cron (no_agent script) that deletes empty sessions
# ~/.hermes/scripts/opencode-empty-session-cleanup.sh (concept)
sqlite3 "$OPENCODE_SESSION_DB" \
  "DELETE FROM sessions WHERE id NOT IN (SELECT DISTINCT session_id FROM session_message);"
```

> ๐Ÿฉน This is the same class of issue documented in [OpenClaw Gateway Down: SQLite Plugin Conflict & Recovery](/blog/openclaw-gateway-sqlite-plugin-conflict/) โ€” SQLite-backed agents need defensive triggers.

---

## ๐Ÿงช Step 8 โ€” Smoke Test

```bash
# 1. OpenCode binary resolves
which opencode

# 2. Headless server is listening
pgrep -af 'opencode serve'
curl -s --max-time 8 http://127.0.0.1:8755/health   # if exposed

# 3. 9Router reachable with key (placeholder)
curl -s --max-time 8 -H "Authorization: Bearer " \
  http://192.168.51.115:20128/v1/models | head -c 200

# 4. Telegram bot service active
systemctl --user is-active opencode-telegram-bot.service

# 5. GBrain MCP healthy
/home/ryan/.bun/bin/gbrain get_health
```

---

## ๐Ÿฉบ Troubleshooting

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `opencode: command not found` | Not on PATH | Use `/home/ryan/.hermes/node/bin/opencode` or `npm link` |
| `NOT NULL constraint failed: session_message.seq` | Empty session row | Apply the trigger + cron watchdog above |
| Model calls 401 | Wrong/expired key | Re-export `` / `` |
| Bot silent | Bad `TELEGRAM_BOT_TOKEN` | Re-paste placeholder, restart service |
| MCP tools missing | gbrain down / bad env | `gbrain get_health`, fix `` |

---

## ๐Ÿ” Security Checklist

- [ ] `TELEGRAM_BOT_TOKEN`, `NINEROUTER_API_KEY`, `OPENROUTER_API_KEY` are ``s, env-injected, never committed.
- [ ] GBrain Postgres URLs are placeholders; DB user is least-privilege.
- [ ] `opencode serve` binds `127.0.0.1` (or behind a proxy), not `0.0.0.0` unprotected.
- [ ] `sandbox_workspace_write.writable_roots` scoped to `/home/ryan`.
- [ ] SQLite trigger + watchdog in place to prevent session crashes.

---

## โœ… Summary

You now have the **OpenCode Agent** wired into your AI stack:

- Installed as a global Node CLI (`~/.hermes/node/bin/opencode`).
- Runs **headless** via `opencode serve` for HTTP/client access.
- Bridges to **Telegram** through `opencode-telegram-bot` (managed systemd service).
- Routes models through **9Router** (primary) with **OpenRouter** fallback โ€” both free.
- Connected to **GBrain** via MCP for shared memory across all agents.
- Hardened against the **SQLite `session_message.seq`** crash with a trigger + cleanup cron.

OpenCode completes the coding trio alongside [Pi](/blog/pi-coding-agent-setup/) (CLI coder) โ€” all sitting behind [OpenClaw](/blog/openclaw-setup/) (gateway) and orchestrated with [Hermes](/blog/hermes-setup/), leaning on [9Router](/blog/9router-setup/) for inference. ๐Ÿš€

---

*No API keys, tokens, or secrets are included in this guide โ€” every value is a `` you supply locally. Built with Astro, GitHub, and Cloudflare Pages.*