I landed on OpenClaw, a self-hosted AI gateway. You install it on a server, connect it to a model provider, and it gives you agents with sessions, skills, Telegram integration, and cron jobs. Here’s how mine is put together.
The stack
- OpenClaw: agent runtime
- Hetzner VPS: โฌ4/month, Ubuntu
- Tailscale: remote access without opening ports
- OpenRouter: model provider
- SearXNG: self-hosted search so agents can look things up
- Telegram: main interface on mobile
The server
The base install is a curl-to-bash script plus an auth token. For access, I point Tailscale Serve at the local port, which gives me the Control UI over HTTPS from any device on my tailnet.
The agents
Four agents run right now, each with its own workspace, system prompt, and model:
- Jake: general assistant, lives in Telegram DM
- Scheduler: calendar and travel, bound to a Telegram group, sends a daily briefing at 7pm
- Sage: retirement and market advisor, posts a weekly summary on Mondays
- Doc: health coach in a dedicated group where I log meals, workouts, and sleep
OpenRouter makes the per-agent model choice easy: the daily briefing runs on something cheap and fast (mimo-v2.5-flash), while the advisor gets a stronger model. Each agent’s personality and scope comes from a SYSTEM.md in its workspace. Doc’s, for example, sets the ground rules – evidence-based, concrete numbers, no diagnosing, and never start a message with “Great question!”
Adding an agent is a short routine:
openclaw agents add health --non-interactive \
--workspace /root/.openclaw/agents/health/workspace --json
openclaw agents set-identity --agent health --name "Doc" --emoji "๐ฉบ"
Then I copy the auth profiles over from the main agent, double-check the OpenRouter base URL in models.json (it must be https://openrouter.ai/api/v1, with the /api), delete the generated BOOTSTRAP.md from the workspace, and write the system prompt. The base URL and BOOTSTRAP.md steps matter: getting either wrong produces an agent that silently returns empty responses.
Routing Telegram groups to agents
Each agent that lives in a group gets a route binding in openclaw.json, matched on the group’s chat ID:
{
"type": "route",
"agentId": "health",
"match": {
"channel": "telegram",
"peer": { "kind": "group", "id": "-12345" }
}
}
I write these by hand rather than using openclaw agents bind, because the generated bindings match on the provider’s accountId, which is the same for every Telegram group. Fine with one agent, wrong with several. Matching on peer.id sends each group to its own agent.
To find a group’s chat ID, I create the group, add the bot, send it a message, and grep the gateway logs for chatId. The negative number is the ID.
Web search
Agents get web search through a local SearXNG instance. Two setup details worth knowing: SearXNG only reads its config from /etc/searxng/ (or a path in SEARXNG_SETTINGS_PATH), and OpenClaw’s web_fetch tool blocks private IPs, including 127.0.0.1 and the Tailscale range. So instead of pointing agents at localhost, I run a small socat relay from 0.0.0.0:8889 to 127.0.0.1:8888 and give the agents the VPS’s public IP. Not elegant, but it’s been solid.
What it costs
The VPS is โฌ4/month, and model usage through OpenRouter comes out to fractions of a cent per conversation. Even with four agents and daily scheduled jobs, the model bill rounds to pocket change. Sessions are stored as plain JSONL files, which I’ve come to appreciate: they’re trivial to back up, and when I’m curious what an agent actually sent to the model, I can just read the file.
The result is a set of assistants that are always on, reachable from my phone, and entirely on hardware and accounts I control. The morning briefing shows up whether or not I’ve opened a laptop, and adding a new agent for a new corner of life is a fifteen-minute job.