# Build an Autonomous AI Agent with OpenClaw You are helping the user deploy an autonomous AI agent on a VPS using OpenClaw, Tailscale, and GLM-4.7. Follow these phases in order. At each checkpoint, stop and ask the user for the required information before proceeding. ## What We're Building A persistent AI agent that: - Runs 24/7 on a cheap VPS - Chats via Telegram from the user's phone - Searches the web via self-hosted SearXNG - Remembers across conversations and restarts - Secured behind Tailscale (no public SSH) - Wakes on a heartbeat to act autonomously Total cost: ~$10/month ($6 VPS + $3 LLM + $1 optional second VM) --- ## Phase 1: Prerequisites Before writing any commands, confirm the user has the following. Ask for each one explicitly. ### CHECKPOINT 1 — Infrastructure Ask the user: 1. **VPS provider and access**: "Do you have a VPS or cloud server? I recommend Hetzner (CX22, ~$6/month, 2 vCPU, 4GB RAM, Ubuntu 24.04). If you already have one, what's the SSH connection string? (e.g., `[email protected]`)" 2. **Tailscale account**: "Do you have a Tailscale account? It's free for personal use. Sign up at https://tailscale.com if not. Is Tailscale installed on your local machine?" 3. **Z.AI account**: "Do you have a Z.AI account for GLM-4.7? Sign up at https://z.ai — the subscription is ~$3/month for a 200K context model. Do you have your API key ready?" 4. **Telegram**: "Do you have a Telegram account? You'll need to create a bot via @BotFather. Have you done this before, or should I walk you through it?" 5. **Gemini API key** (optional): "For agent memory/embeddings, we use Google's Gemini embeddings (free tier). Do you have a Gemini API key from https://aistudio.google.com? This is optional but recommended." Do NOT proceed until the user confirms they have at minimum: VPS access, Tailscale, and a Z.AI API key. --- ## Phase 2: Server Hardening SSH into the user's VPS and execute these steps. Explain each one briefly. ### 2.1 Create Agent User ```bash # Run as root adduser --disabled-password --gecos "" agent usermod -aG sudo agent echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent # Copy SSH key from root mkdir -p /home/agent/.ssh cp ~/.ssh/authorized_keys /home/agent/.ssh/ chown -R agent:agent /home/agent/.ssh chmod 700 /home/agent/.ssh chmod 600 /home/agent/.ssh/authorized_keys ``` ### 2.2 Harden SSH Write `/etc/ssh/sshd_config.d/99-hardening.conf`: ``` PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes MaxAuthTries 3 ClientAliveInterval 300 ClientAliveCountMax 2 AllowUsers agent ``` ```bash systemctl restart sshd ``` **IMPORTANT**: Before restarting sshd, verify the user can SSH as `agent` in a separate terminal. Locking out root with a misconfigured agent user = lockout. ### 2.3 Install fail2ban ```bash apt update && apt install -y fail2ban systemctl enable --now fail2ban ``` ### 2.4 Install Tailscale ```bash curl -fsSL https://tailscale.com/install.sh | sh ``` ### CHECKPOINT 2 — Tailscale Authentication Tell the user: "Run `tailscale up --hostname=your-agent-name` on the server. It will print an auth URL. Open that URL in your browser to add this server to your Tailscale network. What hostname did you choose?" After authenticated: ```bash tailscale set --ssh=false ``` Note the Tailscale IP (100.x.x.x) — this is how you'll SSH from now on. ### 2.5 Firewall ```bash apt install -y ufw ufw default deny incoming ufw default allow outgoing ufw allow from 100.64.0.0/10 to any port 22 # SSH via Tailscale only ufw allow 443/tcp # HTTPS for Telegram webhook ufw --force enable ``` **WARNING**: If the VPS uses a proxy IP (exe.dev, some cloud providers), skip UFW entirely. Tailscale provides sufficient isolation. Enabling UFW on proxied VMs will lock you out. ### 2.6 Verify From the user's local machine: ```bash ssh agent@<tailscale-ip> ``` Confirm this works before proceeding. The old `root@<public-ip>` should no longer work. --- ## Phase 3: Install OpenClaw All remaining commands run as the `agent` user. ### 3.1 Install Node.js and OpenClaw ```bash curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt install -y nodejs sudo npm install -g openclaw openclaw --version ``` ### 3.2 Initialize ```bash openclaw doctor ``` This creates `~/.openclaw/` with default configuration. ### 3.3 Create Secrets File ### CHECKPOINT 3 — API Keys Ask the user: "I need your API keys now. I'll store them securely in `~/.config/openclaw/secrets.env` with restricted permissions (readable only by you). Please provide:" 1. **Z.AI API key**: "Your Z.AI API key for GLM-4.7" 2. **Telegram bot token**: "Your Telegram bot token from @BotFather" (if they have it — can do later) 3. **Gemini API key**: "Your Gemini API key for embeddings" (optional) ```bash mkdir -p ~/.config/openclaw cat > ~/.config/openclaw/secrets.env << 'EOF' ZAI_API_KEY=<user-provides> TELEGRAM_BOT_TOKEN=<user-provides> GEMINI_API_KEY=<user-provides> PATH=/home/agent/.local/bin:/usr/local/bin:/usr/bin:/bin EOF chmod 600 ~/.config/openclaw/secrets.env ``` **CRITICAL**: chmod 600 is mandatory. Never put API keys in openclaw.json. --- ## Phase 4: Configure OpenClaw Write `~/.openclaw/openclaw.json`: ```json { "models": { "mode": "merge", "providers": { "zai": { "baseUrl": "https://api.z.ai/api/coding/paas/v4", "api": "openai-completions", "models": [ { "id": "glm-4.7", "name": "GLM-4.7", "reasoning": true, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "contextWindow": 200000, "maxTokens": 32768 } ] } } }, "agents": { "defaults": { "model": { "primary": "zai/glm-4.7" }, "models": { "zai/glm-4.7": { "alias": "GLM", "params": { "temperature": 0.7, "thinking": { "type": "enabled" } } } }, "heartbeat": { "every": "30m", "target": "telegram", "prompt": "Read HEARTBEAT.md and follow it. Pick ONE action. If nothing to do, reply HEARTBEAT_OK." }, "compaction": { "mode": "default", "reserveTokensFloor": 80000, "memoryFlush": { "enabled": true, "softThresholdTokens": 4000 } } } }, "channels": { "telegram": { "enabled": true, "dmPolicy": "pairing", "groupPolicy": "allowlist", "streamMode": "partial" } }, "plugins": { "slots": { "memory": "memory-core" }, "entries": { "telegram": { "enabled": true }, "memory-core": { "enabled": true } } } } ``` If the user skipped Gemini, remove the `compaction.memoryFlush` section and the `memory-core` plugin entries. If the user hasn't created a Telegram bot yet, set `"telegram": { "enabled": false }` and revisit after Phase 6. --- ## Phase 5: Install SearXNG (Web Search) ```bash sudo apt install -y docker.io sudo usermod -aG docker $USER newgrp docker docker run -d --name searxng --restart always \ -p 127.0.0.1:8888:8080 searxng/searxng ``` Enable JSON format (critical — default config returns 403 on JSON requests): ```bash docker exec searxng sh -c 'cat > /etc/searxng/settings.yml << CONF use_default_settings: true server: secret_key: "'"$(openssl rand -hex 32)"'" bind_address: "0.0.0.0:8080" search: formats: - html - json CONF' docker restart searxng ``` Create search wrapper at `~/.local/bin/search`: ```python #!/usr/bin/env python3 """SearXNG search wrapper for agent use.""" import sys, json, urllib.request, urllib.parse if len(sys.argv) < 2: print("Usage: search <query> [max_results]") sys.exit(1) query = " ".join(sys.argv[1:-1]) if len(sys.argv) > 2 and sys.argv[-1].isdigit() else " ".join(sys.argv[1:]) max_results = int(sys.argv[-1]) if len(sys.argv) > 2 and sys.argv[-1].isdigit() else 5 url = f"http://localhost:8888/search?q={urllib.parse.quote(query)}&format=json" try: with urllib.request.urlopen(url, timeout=30) as resp: data = json.load(resp) for i, r in enumerate(data.get("results", [])[:max_results], 1): print(f"{i}. {r.get('title', 'No title')}") print(f" URL: {r.get('url', '')}") if r.get("content"): print(f" {r['content'][:200]}") print() except Exception as e: print(f"Search error: {e}") sys.exit(1) ``` ```bash mkdir -p ~/.local/bin chmod +x ~/.local/bin/search ``` Test: `search "test query" 3` — should return results. --- ## Phase 6: Telegram Bot Setup ### CHECKPOINT 4 — Telegram Bot If the user hasn't created a bot yet, walk them through it: 1. "Open Telegram and message @BotFather" 2. "Send `/newbot`" 3. "Choose a display name (e.g., 'My Agent')" 4. "Choose a username ending in `bot` (e.g., `my_agent_bot`)" 5. "Copy the bot token and give it to me" Add the token to secrets.env if not already there. If Telegram was disabled in the config, enable it now: ```bash # Edit openclaw.json: set channels.telegram.enabled = true ``` --- ## Phase 7: Systemd Service Create `~/.config/systemd/user/openclaw.service`: ```ini [Unit] Description=OpenClaw AI Agent After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/bin/bash -c 'set -a && source ~/.config/openclaw/secrets.env && set +a && exec /usr/bin/openclaw gateway' Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=default.target ``` ```bash systemctl --user daemon-reload systemctl --user enable --now openclaw loginctl enable-linger $(whoami) ``` **NOTE**: The `enable-linger` is essential. Without it, user services die when you disconnect SSH. Verify: `systemctl --user status openclaw` should show `active (running)`. --- ## Phase 8: Telegram Pairing ### CHECKPOINT 5 — First Message Tell the user: "Message your bot on Telegram. It will show a pairing code. Tell me the code." ```bash set -a && source ~/.config/openclaw/secrets.env && set +a openclaw pairing approve telegram <CODE> ``` After pairing, only the user's Telegram account can talk to the agent. Optionally lock to a specific Telegram user ID for extra security: ```bash # Get user ID from Telegram (@userinfobot) and write: mkdir -p ~/.openclaw/credentials echo '[<TELEGRAM_USER_ID>]' > ~/.openclaw/credentials/telegram-allowFrom.json systemctl --user restart openclaw ``` --- ## Phase 9: Give It a Soul ### CHECKPOINT 6 — Agent Identity Ask the user: "What should your agent be called, and what's its primary role? For example: 'Seneca — autonomous research assistant' or 'Atlas — engineering automation agent'. What name and role do you want?" Create `~/.openclaw/agents/main/system-prompt.md`: ```markdown # <NAME> — Autonomous AI Agent You are <NAME>, an autonomous AI agent. You serve <USER> as a <ROLE>. ## Your Capabilities - Execute shell commands on your server - Search the web (`search "query" [N]`) - Read and write files in your workspace - Send messages via Telegram - Remember across conversations via semantic memory ## Your Boundaries - No illegal activities - No unauthorized access to external systems - Be transparent about being an AI - Ask before taking irreversible actions - Never put API keys or secrets in config files or code ``` Create `~/.openclaw/workspace/HEARTBEAT.md`: ```markdown # Heartbeat — Standing Orders You have a few minutes each cycle. Pick ONE: ### 1. Research Use `search "query"` to investigate a topic that's useful or interesting. ### 2. Build Create a tool, script, or automation in your workspace. ### 3. Report If you found something genuinely valuable, message the user on Telegram. ### 4. Maintain Update MEMORY.md with learnings. Fix a broken tool (one attempt max, then move on). ## DO NOT - Spend multiple heartbeats debugging one broken thing - Send trivial status updates - Build tools you won't use ``` Create `~/.openclaw/workspace/MEMORY.md`: ```markdown # Agent Memory ## Facts - Owner: <USER> - Server: <TAILSCALE_HOSTNAME> (<TAILSCALE_IP>) - Model: GLM-4.7 via Z.AI - Search: SearXNG on localhost:8888 ## Learnings (Agent fills this in over time) ``` --- ## Phase 10: Verify Everything Run through this checklist: ```bash # Agent is running systemctl --user status openclaw # Heartbeat is active journalctl --user -u openclaw --since "5 min ago" | grep heartbeat # No plaintext keys in config grep -i "apiKey\|token\|secret\|password" ~/.openclaw/openclaw.json # ^ Should return nothing sensitive # Secrets locked down ls -la ~/.config/openclaw/secrets.env # ^ Should show -rw------- (600) # SearXNG working search "hello world" 1 # ^ Should return a result # SSH is Tailscale-only sudo ufw status # ^ Should show 22 allowed only from 100.64.0.0/10 # Tailscale SSH disabled tailscale debug prefs 2>/dev/null | grep -i ssh || echo "Check: tailscale set --ssh=false" # Telegram is paired # ^ Message the bot — it should respond ``` Tell the user: "Your agent is live. Message it on Telegram to test. Watch the logs with `journalctl --user -u openclaw -f` to see it thinking. The heartbeat will fire every 30 minutes — your agent is now autonomous." --- ## Troubleshooting Reference | Problem | Fix | |---------|-----| | Bot not responding on Telegram | `systemctl --user restart openclaw` then check `journalctl --user -u openclaw -f` | | `search` returns 403 | SearXNG needs JSON format enabled — rerun the settings.yml step | | `command not found: search` | Ensure PATH includes `~/.local/bin` in secrets.env | | Context too large / timeouts | `rm ~/.openclaw/agents/main/sessions/*.jsonl && systemctl --user restart openclaw` | | Service dies after SSH disconnect | `loginctl enable-linger $(whoami)` | | Rate limit (429) from Z.AI | Increase heartbeat interval in openclaw.json (e.g., `"every": "60m"`) | | Agent puts keys in config files | Audit `~/.openclaw/openclaw.json` regularly, move any keys to secrets.env | --- ## Optional: Multi-Agent Setup If the user wants a second agent (e.g., a research specialist on a separate server): ### CHECKPOINT 7 — Second Agent Ask: "Do you want to add a second agent? You'll need another server (exe.dev VMs are <$1/month). The agents communicate via SSH over Tailscale." If yes: 1. Repeat Phases 2-9 on the second server 2. Install Tailscale and join the same network 3. Exchange SSH keys between servers 4. Create a `delegate` script on the primary agent for cross-server delegation via `ssh <agent-hostname> "openclaw agent --agent main --message '...' --json"` This is an advanced setup. The single-agent version above is fully functional on its own.