# 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.