# ๐ง 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.*