---
# System prepended metadata

title: 'Setting up Hermes: a self-hosted always-on AI agent'

---

# Setting up Hermes: a self-hosted always-on AI agent

This is a complete walkthrough for setting up [Hermes](https://hermes-agent.nousresearch.com) on a DigitalOcean Droplet. Built from the trial-and-error of doing it once. Includes all the gotchas you'll hit.

**What you'll have at the end:**
- A 24/7 AI agent running on a $32/mo cloud server
- Reachable from anywhere via Discord and Telegram
- Cross-channel shared memory (one brain, many surfaces)
- GPT-5.5 subsidized by your $20/mo ChatGPT Plus subscription
- OpenRouter fallback so it never goes silent
- Full webapp dev environment (Next.js, Supabase, Vercel, GitHub) configured so Hermes can scaffold projects autonomously
- Project-aware context routing (each Discord channel = one project)
- Tailscale-based private networking (no public ports open)
- Cursor IDE pointed at the server like it's local
- Survives reboots automatically

**Total cost:** ~$52/mo (Droplet $32 + ChatGPT Plus $20 if you don't already have it)

**Time:** 2-3 hours, including signups and waiting

**Prerequisites:**
- A Mac (this guide is Mac-specific in a few places — Linux/Windows would need small adjustments)
- A credit card (for DigitalOcean and ChatGPT Plus)
- Existing accounts: GitHub, Discord, Telegram (will create others as we go)
- Will need: a ChatGPT Plus subscription ($20/mo). Without this, GPT-5.5 via Codex OAuth won't work and you'll need to budget for OpenRouter pay-per-token usage instead

---

## Terminal legend

You'll be juggling **two** terminals throughout this guide. Confusing them is the #1 cause of pasting commands in the wrong place.

| Tag | Terminal | When to use |
|---|---|---|
| `[Mac]` | macOS Terminal.app on your Mac | Initial SSH, OAuth tunneling for Codex login |
| `[Cursor]` | Cursor's integrated terminal (a shell on the Droplet) | Almost everything else |
| `[Browser]` | Anything that's clicking/signing up in your browser | Self-explanatory |
| `[Discord]` | Sending a message in Discord | Once Hermes is set up |

---

# Part 1: DigitalOcean Droplet (10 min)

## 1.1 — Create account

`[Browser]` [digitalocean.com](https://digitalocean.com) → Sign Up. Google SSO is fastest. Small pending auth on your card (~$1), usually instant.

**Look for a promo credit** at signup — new accounts often get $200 / 60 days free, which would cover this setup for ~5 years.

> **Why DigitalOcean and not Hetzner?** Hetzner is cheaper but their signup verification rejects a lot of international users (especially from Mexico, Latin America, Asia) for opaque "fraud" reasons. DigitalOcean has clean signup from anywhere.

## 1.2 — Set a billing cap

Settings → Billing → set an alert at $50/mo. Insurance against surprise bills if you accidentally leave Droplets running.

## 1.3 — Create a project

Sidebar → Projects → New Project → name it `hermes`.

## 1.4 — Add your SSH key

`[Mac]` Open Terminal.app (Cmd+Space → "Terminal").

Check if you have an SSH key:

```bash
ls ~/.ssh/id_ed25519.pub
```

If "No such file or directory," generate one:

```bash
ssh-keygen -t ed25519 -C "your-email@example.com"
```

Press Enter to accept defaults. **Set a passphrase** (recommended).

Print it:

```bash
cat ~/.ssh/id_ed25519.pub
```

`[Browser]` In DO console → Settings → Security → SSH Keys → Add SSH Key → paste the output → name it `laptop`.

## 1.5 — Create the Droplet

`[Browser]` Create → Droplets:

- **Region:** SFO3 (San Francisco) for West Coast / Latin America. NYC for East Coast. Frankfurt for Europe.
- **OS:** Ubuntu 24.04 (LTS) x64
- **Size:** Premium Intel — **$32/mo** (2 vCPU, 4 GB RAM, 120 GB NVMe SSD)
- **Authentication:** SSH Key → select `laptop`
- **Hostname:** `hermes-01`

Click Create Droplet. Note the public IPv4 address.

> **Why this size?** The $12 Droplet (1 vCPU, 2 GB RAM) is enough for Hermes-as-chat-agent but tight if you want it building webapps. The $32 Premium gives you headroom for `npm install`, dev servers, and headless Chrome without OOM kills. **You can resize later** without recreating — DO supports power-off, change-size, power-on in 2 minutes.

## 1.6 — First SSH in

`[Mac]`:

```bash
ssh root@YOUR_DROPLET_IP
```

Type `yes` to accept the fingerprint. You should land at `root@hermes-01-droplet:~#`.

**Keep this terminal open.**

---

# Part 2: Server hardening + dev tooling (15 min)

`[Mac, in your root SSH session]` from here through Part 3.

## 2.1 — Create non-root user

Don't run Hermes as root.

```bash
adduser hermes
usermod -aG sudo hermes
```

Set a strong password when prompted. **Save it in your password manager** — you'll use it for `sudo` later.

## 2.2 — Add swap

The 4 GB Droplet has no swap by default. Without it, a single OOM kill takes Hermes offline.

```bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf
```

## 2.3 — Update and install essentials

```bash
apt update && apt upgrade -y
apt install -y ufw curl unattended-upgrades git build-essential
```

Accept defaults on any config prompts. `build-essential` matters because some npm packages (sqlite, sharp, bcrypt) need a C compiler.

> **You'll see "system restart required" messages after this.** That's fine, ignore them — we'll reboot at the end of the whole setup.

## 2.4 — Install Docker

For when Hermes needs to run databases, headless browsers, or sandboxed builds.

```bash
curl -fsSL https://get.docker.com | sh
usermod -aG docker hermes
```

## 2.5 — Enable automatic security updates

```bash
dpkg-reconfigure -plow unattended-upgrades
```

Pick Yes.

## 2.6 — Cap systemd journal size

So logs don't eventually eat your disk.

```bash
sed -i 's/#SystemMaxUse=/SystemMaxUse=500M/' /etc/systemd/journald.conf
systemctl restart systemd-journald
```

## 2.7 — Prep firewall (don't enable yet)

```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH - temporary'
```

> **Don't `ufw enable` yet.** We'll do that after Tailscale is working, otherwise you'll lock yourself out.

---

# Part 3: Tailscale (15 min)

Tailscale puts your Droplet on a private mesh network. After this, no public ports will be open at all — your Droplet becomes invisible to the internet, reachable only from devices on your Tailnet.

## 3.1 — Create Tailscale account

`[Browser]` [tailscale.com](https://tailscale.com) → Sign up (Google/GitHub/Microsoft SSO). Free personal plan, 100 devices.

## 3.2 — Install Tailscale on your Mac first

`[Mac]`:

```bash
brew install --cask tailscale
```

Open the Tailscale app from /Applications. Sign in. The icon appears in your menu bar.

Verify:

```bash
tailscale status
```

> **Common issue:** "command not found" on the Mac CLI. The Mac App Store version doesn't put the CLI in your PATH automatically. Install the shim:
> ```bash
> sudo /Applications/Tailscale.app/Contents/MacOS/Tailscale install-shim
> ```

## 3.3 — Enable MagicDNS (critical step that's easy to miss)

By default, Tailscale doesn't resolve device names like `hermes`. We need MagicDNS.

`[Browser]` [login.tailscale.com/admin/dns](https://login.tailscale.com/admin/dns):

1. If "Nameservers" is empty: click **Add nameserver** → pick **Cloudflare** (or any) → save.
2. Find the **MagicDNS** section → click **Enable MagicDNS**.

> **If you have another VPN running on your Mac** (Cloudflare WARP, Mullvad, NordVPN, etc.), it will interfere with Tailscale's DNS and routing. **Disconnect any other VPN before continuing.** This is the single most common failure mode for the rest of this guide.

## 3.4 — Install Tailscale on the Droplet

`[Mac, root SSH session on the Droplet]`:

```bash
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up --ssh
```

The `--ssh` flag is important: Tailscale will handle SSH authentication for Tailnet peers, so you won't need separate SSH keys for the `hermes` user.

The command prints a URL. Open it in your **Mac browser** → authenticate.

## 3.5 — Name the Droplet, disable key expiry

`[Browser]` [login.tailscale.com/admin/machines](https://login.tailscale.com/admin/machines):

1. Click your Droplet → rename to `hermes`
2. Click "..." menu → **Disable key expiry** (otherwise the Droplet drops off your Tailnet every 180 days)

## 3.6 — Test Tailscale SSH

`[Mac]` Open a NEW Terminal window:

```bash
ssh hermes@hermes
```

You should land in a shell as the `hermes` user — no password needed (Tailscale handles auth).

> **If `ssh hermes@hermes` fails with "Could not resolve hostname"**, MagicDNS isn't working on your Mac. Quick workarounds:
>
> Force Tailscale DNS:
> ```bash
> sudo tailscale up --accept-dns --accept-routes
> sudo dscacheutil -flushcache
> sudo killall -HUP mDNSResponder
> ```
>
> Or just use the IP for now — find it with `tailscale status` (looks like `100.x.x.x`):
> ```bash
> ssh hermes@100.x.x.x
> ```
>
> Or hardcode the hostname in `/etc/hosts`:
> ```bash
> echo "100.x.x.x hermes" | sudo tee -a /etc/hosts
> ```

> **If you get `Host key verification failed`**, SSH is being cautious because it remembers the same fingerprint from earlier. Type `yes` when it asks "Are you sure you want to continue connecting?"

## 3.7 — Lock down public SSH

`[Mac, in your root SSH session — NOT the new Tailscale one]`:

```bash
ufw allow in on tailscale0
ufw delete allow 22/tcp
ufw enable
```

Confirm with `y`. Verify:

```bash
ufw status
```

Only `tailscale0` should allow inbound. Public SSH (port 22 to the world) is closed.

## 3.8 — Verify lockdown

`[Mac]`:

```bash
ssh root@YOUR_DROPLET_PUBLIC_IP
```

Should hang (no response = good). Then:

```bash
ssh hermes@hermes
```

Should work. Your Droplet is now invisible to the public internet.

**Exit the root session.** From here on you only use `ssh hermes@hermes`.

---

# Part 4: Cursor IDE Remote-SSH (5 min)

This makes the Droplet feel local. Files, editor, terminal — all in one window pointed at the remote server.

## 4.1 — Install Cursor

`[Browser]` [cursor.com](https://cursor.com) → Download → install. (VS Code with the Remote-SSH extension works equivalently if you prefer.)

## 4.2 — Connect via SSH

Open Cursor. On the welcome screen, click **Connect via SSH** (third tile, top-right).

When prompted for the SSH command, type:

```
ssh hermes@hermes
```

Pick `~/.ssh/config` to save the host config.

A new Cursor window opens. Bottom-left shows a spinning indicator → "Installing Cursor Server" (~30 sec, one-time) → eventually a green **SSH: hermes** indicator.

It may prompt:
- **Host fingerprint confirmation** → type `yes`
- **Password** → enter the `hermes` user password from step 2.1

## 4.3 — Open the home folder

In the connected window: File menu → Open Folder → type `/home/hermes` → Open.

The left sidebar now shows the Droplet's home directory. Open the integrated terminal with **Ctrl+\`** (Control + backtick).

Test:

```bash
whoami
pwd
```

Should show `hermes` and `/home/hermes`.

**Cursor's integrated terminal is now `[Cursor]` for the rest of the guide.**

---

# Part 5: Install Hermes (10 min)

`[Cursor]` from here on, unless marked otherwise.

## 5.1 — Activate Docker group

You added `hermes` to the docker group in 2.4, but the current session doesn't have it active. Reload Cursor's connection:

Cmd+Shift+P → type **Reload Window** → Enter.

After Cursor reconnects, in the new terminal:

```bash
docker run --rm hello-world
```

Should print "Hello from Docker!"

## 5.2 — Run the Hermes installer

```bash
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
```

Takes 3-5 minutes. The installer handles all dependencies (Python, Node.js, ripgrep, ffmpeg), clones the repo, creates a virtual environment, sets up the `hermes` command.

After it finishes:

```bash
exec bash
hermes --version
```

If "command not found":

```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
hermes --version
```

## 5.3 — Skip through the setup wizard

When you first run `hermes`, an interactive wizard launches asking you to set up provider, model, and messaging in one go. **We're going to skip it and configure everything explicitly** because the wizard can't do the SSH port forwarding we'll need for Codex OAuth.

If the wizard pops up:

1. Pick **Full setup**
2. For each section, select **Leave unchanged** or **Skip**
3. When asked about messaging platforms, scroll to **Done** at the bottom of the list
4. Press **Enter** through any "Maximum iterations" or "Tool progress mode" prompts (defaults are fine)

Hermes will boot and say "Welcome to Hermes Agent!" with a default model. **Type `/quit` to exit** and we'll configure properly in the next sections.

---

# Part 6: Codex OAuth + ChatGPT Plus (20 min)

This is where GPT-5.5 gets wired up via your ChatGPT Plus subscription instead of paying per-token API rates.

## 6.1 — Subscribe to ChatGPT Plus (if you don't have it)

`[Browser]` [chatgpt.com/pricing](https://chatgpt.com/pricing) → **Get Plus** → $20/mo.

> **Critical:** Use the OpenAI account you'll use in the next steps. The subscription IS the subsidy — without it, you'll get HTTP 400 errors when trying to use any model via Codex OAuth.

> **Note on ChatGPT Plus vs Business:** Plus is the right pick for personal/agent use. ChatGPT Business is for teams using ChatGPT in the chat UI with shared workspaces, admin console, SSO. Hermes doesn't use any of that — it just hits the Codex API surface, where Plus access is the same.

## 6.2 — Install Codex CLI

`[Cursor]`:

```bash
npm install -g @openai/codex
codex --version
```

> **If npm gives EACCES errors:**
> ```bash
> mkdir -p ~/.npm-global
> npm config set prefix ~/.npm-global
> echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
> source ~/.bashrc
> npm install -g @openai/codex
> ```

## 6.3 — The OAuth tunnel (Mac Terminal needed)

`codex login` opens a browser redirect to `localhost:1455`. On the headless Droplet, that "localhost" is the Droplet itself. We need to tunnel that callback back to your Mac's browser.

`[Mac]` Open Terminal.app — a fresh window:

```bash
ssh -L 1455:localhost:1455 hermes@hermes
```

In that tunneled session:

```bash
codex login
```

It prints:
- A `localhost` server confirmation
- A long auth URL

Copy the URL, paste in your **Mac browser**. Log in with your ChatGPT Plus account. Click through consent. The browser redirects to `http://localhost:1455` — your SSH tunnel forwards it to the Droplet, and you'll see "Signed in to Codex" in the browser.

Press **Ctrl+C** in the Mac Terminal to exit `codex login` (it doesn't auto-exit cleanly).

Verify:

```bash
ls ~/.codex/auth.json
exit
```

Close the Mac Terminal window. **Save this re-auth recipe** in your password manager — you'll need it every 1-3 months when the OAuth token expires:

```
ssh -L 1455:localhost:1455 hermes@hermes
codex login
sudo systemctl restart hermes-gateway
```

## 6.4 — Point Hermes at Codex

`[Cursor]`:

```bash
hermes model
```

Walk through the wizard:

- **Provider:** OpenAI Codex
- **Auth:** OAuth (it should auto-detect `~/.codex/auth.json` and offer to import — say `y`)
- **Default model:** **`gpt-5.5`** if listed; otherwise the latest model that's NOT a `-codex` suffix.

> **Important:** When the wizard offers `gpt-5.5-codex`, that doesn't exist as of writing. Pick plain `gpt-5.5`. If you pick a non-existent model, the agent will fail with HTTP 400.

## 6.5 — Smoke test

```bash
hermes
```

Then in the Hermes prompt:

```
What's the kernel version of this machine?
```

It should run `uname -r` and report something like `6.8.0-71-generic`. Status bar at the bottom should show `gpt-5.5`.

> **If you get HTTP 400 "model not supported when using Codex with a ChatGPT account":** your OpenAI account doesn't have an active Plus/Pro/Business subscription, OR you picked a model the Codex tier doesn't support. Verify Plus is active and try `gpt-5.3-codex` as a fallback.

`/quit` to exit.

---

# Part 7: OpenRouter fallback (10 min)

Even with Plus active, OAuth tokens occasionally fail to refresh. Without a fallback, Hermes goes silent at 3am. OpenRouter is a $5 safety net that swaps in seamlessly when Codex hiccups.

## 7.1 — Get an OpenRouter key

`[Browser]` [openrouter.ai](https://openrouter.ai) → Sign up (Google SSO is fastest) → **Credits** → add **$5-10**. Then **Keys** → **Create Key** → name `hermes-fallback` → copy the key.

The key starts with `sk-or-v1-...`. **Save it.** You can't view it again later, only revoke and regenerate.

## 7.2 — Add the key to Hermes's env

`[Cursor]` In the file tree, expand `.hermes/` (toggle hidden files with **Cmd+Shift+.** if needed). Click `.env`. If it doesn't exist, create it.

Add this line at the bottom (paste your real key):

```
OPENROUTER_API_KEY=sk-or-v1-your-actual-key-here
```

Save (Cmd+S). Then in the terminal:

```bash
chmod 600 ~/.hermes/.env
```

> **Important:** The variable name MUST be `OPENROUTER_API_KEY` exactly. If you (or Hermes) accidentally name it `OPENAI_API_KEY` because that's what the prompt suggested, the fallback won't work.

## 7.3 — Configure the fallback in config.yaml

`[Cursor]` Click `.hermes/config.yaml` in the sidebar.

Find the `fallback_providers: []` line near the top. Replace it with:

```yaml
fallback_providers:
  - name: openrouter
    provider: openrouter
    model: openai/gpt-5.4
    api_key: ${OPENROUTER_API_KEY}
    base_url: https://openrouter.ai/api/v1
```

Save (Cmd+S).

## 7.4 — Verify the fallback actually works

This is the test most people skip and regret. Just having a fallback config doesn't mean it fires.

```bash
mv ~/.hermes/auth.json ~/.hermes/auth.json.bak
hermes
```

In the Hermes prompt, type any message:

```
hello
```

Watch the status bar at the bottom — it should show `gpt-5.4` (the fallback) instead of `gpt-5.5`. If it answers, the fallback works.

> **Don't be tricked:** Hermes might still try to use Codex OAuth credentials if you cached them in this same shell session. If status still shows `gpt-5.5`, exit completely with `/quit`, then run `pkill -f hermes` to kill any background process, then try again.

Then `/quit` and restore:

```bash
mv ~/.hermes/auth.json.bak ~/.hermes/auth.json
```

> **Why does Hermes have its OWN `auth.json` separate from `~/.codex/auth.json`?** When you ran `hermes model` earlier and chose to import the Codex credentials, Hermes made its own copy at `~/.hermes/auth.json` so its session is independent. Renaming just the Codex one wouldn't trigger the fallback because Hermes was using its own copy.

---

# Part 8: Webapp dev workspace (10 min)

For Hermes to scaffold and deploy webapps autonomously.

## 8.1 — Projects directory

```bash
mkdir ~/projects
```

## 8.2 — GitHub auth

```bash
sudo apt install -y gh
gh auth login
```

Walk through:
- **GitHub.com**
- **HTTPS**
- **Yes** (authenticate Git with credentials)
- **Login with a web browser**

It prints a one-time code and a URL. Open the URL in your **Mac browser**, paste the code, authorize.

Verify:

```bash
gh auth status
```

> **If you want Hermes to be able to delete repos** (useful for cleanup of test scaffolds): you'll need to refresh the token with the `delete_repo` scope:
> ```bash
> gh auth refresh -h github.com -s delete_repo
> ```
> Trade-off: more agent power, more potential for accidents. You can skip this and delete repos manually when needed.

## 8.3 — Git identity

```bash
git config --global user.name "Your Name"
git config --global user.email "your-email@example.com"
git config --global init.defaultBranch main
```

## 8.4 — Install Vercel and Supabase CLIs

```bash
npm install -g vercel
curl -fsSL https://github.com/supabase/cli/releases/latest/download/supabase_linux_amd64.tar.gz | tar -xz -C /tmp
sudo mv /tmp/supabase /usr/local/bin/
vercel --version && supabase --version
```

## 8.5 — Get Vercel and Supabase API tokens

`[Browser]`:

**Vercel token:**
- [vercel.com/account/tokens](https://vercel.com/account/tokens)
- Create Token → name `hermes-droplet` → no expiration → Full Account scope
- Copy the token

**Supabase token:**
- [supabase.com/dashboard/account/tokens](https://supabase.com/dashboard/account/tokens)
- Generate new token → name `hermes-droplet`
- Copy the token

> **Why API tokens, not OAuth?** OAuth would require another SSH tunnel dance like Codex did. API tokens are simpler, don't expire (unless you set them to), and survive reboots cleanly. The reason we used OAuth for Codex is that OAuth IS the path to the ChatGPT subscription subsidy — Vercel and Supabase don't have that distinction; tokens give the same access.

## 8.6 — Add tokens to Hermes's env

`[Cursor]` Click `.hermes/.env` in the sidebar. Add at the bottom:

```
VERCEL_TOKEN=your-vercel-token-here
SUPABASE_ACCESS_TOKEN=sbp_your-supabase-token-here
```

Save (Cmd+S). Then:

```bash
chmod 600 ~/.hermes/.env
```

## 8.7 — Verify all auth

```bash
gh auth status
export VERCEL_TOKEN=$(grep '^VERCEL_TOKEN=' ~/.hermes/.env | cut -d= -f2-)
vercel whoami --token=$VERCEL_TOKEN
export SUPABASE_ACCESS_TOKEN=$(grep '^SUPABASE_ACCESS_TOKEN=' ~/.hermes/.env | cut -d= -f2-)
supabase orgs list
```

All three should succeed: GitHub username, Vercel email, Supabase org list.

**Copy your Supabase org ID** (the long string under `id`) from the last command — you'll paste it into STACK.md next.

---

# Part 9: Discord + Telegram gateways (20 min)

Setting up both messaging channels at once.

## 9.1 — Create Discord server

`[Browser]` Open Discord ([discord.com/app](https://discord.com/app) or desktop app).

Left sidebar → **+** at the bottom → **Create My Own** → **For me and my friends** → name it `Hermes` (or whatever) → Create.

You're in your private Discord server. Default `#general` channel exists.

## 9.2 — Create Discord bot application

`[Browser]` [discord.com/developers/applications](https://discord.com/developers/applications):

1. **New Application** (top right) → name it `Hermes` → check ToS → **Create**
2. Left sidebar → **Bot**
3. Click **Reset Token** at the top → **Yes, do it!** → copy the token immediately. **You can't view it again.**

### CRITICAL: Enable Privileged Intents

Still on the Bot page, scroll to **Privileged Gateway Intents**. Toggle ON:
- ✅ **MESSAGE CONTENT INTENT** ← required, without this the bot connects but can't read messages
- ✅ PRESENCE INTENT (optional)
- ✅ SERVER MEMBERS INTENT (optional)

**Click Save Changes** at the bottom.

> This is the #1 thing people miss. Without MESSAGE CONTENT INTENT, your bot will appear online but never respond to messages.

## 9.3 — Invite bot to your server

Same Developer Portal, your Hermes app. Left sidebar → **OAuth2** → **URL Generator**.

**Scopes:**
- ✅ `bot`
- ✅ `applications.commands`

**Bot Permissions:** scroll to bottom → ✅ **Administrator** (simplest for a private personal server)

Copy the **Generated URL** at the bottom. Open it in a new browser tab → "Add this app to a server?" → pick your Hermes server → **Authorize**.

The Hermes bot now appears in your server's member list.

## 9.4 — Create project channels in Discord

In your Hermes server, create text channels for the use cases you'll have. Suggested:

- `#general` (already exists)
- `#scratch` — brainstorming, naming projects
- `#random` — casual chat
- `#agent-logs` — for cron/notification delivery
- `#if-internal` — internal/company notes (or whatever your equivalent is)

Each project will get its own channel later (e.g., `#guitar-tuner`, `#tab-tracker`).

## 9.5 — Get your Discord user ID

`[Discord]` Settings (gear icon, bottom-left) → **Advanced** → toggle ON **Developer Mode**.

Right-click on your own name in any channel → **Copy User ID**.

## 9.6 — Create Telegram bot

`[Telegram, on phone or web.telegram.org]`:

Search **`@BotFather`** (the official one with blue checkmark). Start chat. Send:

1. `/newbot`
2. Display name when asked (e.g., `Hermes Personal`)
3. Username when asked — must end in `bot` (e.g., `aluken_hermes_bot`)
4. Copy the API token he replies with — looks like `8123456789:AAEhBP_xxx...`

## 9.7 — Get your Telegram user ID

`[Telegram]` Search `@userinfobot` → start chat. It instantly replies with your info. Copy the number under `Id`.

## 9.8 — Run the gateway setup wizard

`[Cursor]`:

```bash
hermes gateway setup
```

You'll see a list of platforms. Configure both:

### Discord:
- Select **Discord**
- Paste the bot token from 9.2
- **Allowed user IDs**: paste your Discord user ID from 9.5 (CRITICAL — without this, anyone who finds your bot can command it)
- **Home channel ID**: right-click `#general` in Discord → Copy Channel ID → paste it. Or leave blank, set later with `/set-home`.

### Telegram:
- Back to platform list, select **Telegram**
- Paste the bot token from 9.6
- **Allowed user IDs**: paste your Telegram user ID from 9.7
- **Home channel ID**: paste the same Telegram user ID (in DMs, the "channel" is your conversation)

When both are configured, scroll to **Done** → Enter.

The wizard will ask if you want to install as systemd service. Pick **Y**, then **System service**.

## 9.9 — Install the systemd service manually

The wizard punts here because installing a system service requires sudo, which it can't do from inside its own process. Run the commands it tells you:

```bash
sudo /home/hermes/.local/bin/hermes gateway install --system --run-as-user hermes
sudo /home/hermes/.local/bin/hermes gateway start --system
```

> **Why the full path `/home/hermes/.local/bin/hermes`?** When you run `sudo`, your PATH gets reset to system defaults that don't include `~/.local/bin`. Either use the full path, or skip sudo and run directly (which won't work for system services).

Verify:

```bash
sudo systemctl status hermes-gateway
```

Should show **active (running)**. Press `q` to exit the pager.

## 9.10 — Test both gateways

`[Discord, in #general]` send: `hello`

Should reply within ~5 seconds.

> **If it doesn't reply but the service is running**, the most common cause is `MESSAGE CONTENT INTENT` not being enabled. Go back to step 9.2, toggle it on, save, then:
> ```bash
> sudo systemctl restart hermes-gateway
> ```

`[Telegram, DM your bot]` send: `hi`

Should also reply.

> **If Discord requires `@Hermes` mentions to respond**, that's by default. We'll fix this in the next step.

## 9.11 — Tune Discord settings

`[Cursor]` Click `.hermes/config.yaml` in the sidebar. Find the `discord:` section. Change:

```yaml
discord:
  require_mention: false
  free_response_channels: ''
  allowed_channels: ''
  auto_thread: false
  reactions: true
  channel_prompts: {}
  server_actions: ''
```

Save. Restart:

```bash
sudo systemctl restart hermes-gateway
```

Now plain messages (no @-mention) get plain replies, no auto-threading.

> **`auto_thread: false`** is a preference. If you like Discord-thread-per-conversation, leave it `true`. Setting it `false` makes responses stay in the channel, which is cleaner for project work.

## 9.12 — Quick cross-channel memory test

`[Discord, in #guitar-tuner or any channel you created]`:

```
remember: my favorite chord is G major
```

Then in a different channel:

```
what's my favorite chord?
```

If it replies "G major", you've confirmed cross-channel shared memory. **One brain, many surfaces.** This is the architecture you wanted.

---

# Part 10: Multi-project workflow (30 min)

This is what makes Hermes feel magical: each Discord channel = one project, with consistent stack scaffolding across them all.

## 10.1 — Create STACK.md (your stack recipe book)

`[Cursor]`:

```bash
cat > ~/projects/STACK.md << 'STACKEOF'
# Standard project stack

Read this BEFORE scaffolding any new project. Don't trust your memory of it.

## Tech
- Next.js 15 (App Router) + TypeScript + Tailwind CSS
- shadcn/ui for components
- Supabase (Postgres + Auth + Storage)
- Google OAuth via Supabase Auth
- Deploy: Vercel
- Source: GitHub (public unless told otherwise)

## Naming
Channel name = directory slug = repo name = Vercel project = Supabase project. Always lowercase kebab-case.

## Supabase orgs
- DEFAULT: PASTE_DEFAULT_ORG_ID (Personal — side projects)
- BUSINESS: PASTE_BUSINESS_ORG_ID (Company projects)

If the project sounds personal/side, use the default org silently.
If it sounds business/company-related OR is ambiguous, ASK FIRST: "Personal or Business org for this one?" Wait for answer before creating the Supabase project.

## Tokens
VERCEL_TOKEN and SUPABASE_ACCESS_TOKEN are in ~/.hermes/.env (auto-sourced by gateway service). Use --token=$VERCEL_TOKEN flag for vercel commands; SUPABASE_ACCESS_TOKEN env var works for supabase commands.

## Default region
us-west-1 (or closest to the user)

## New project workflow

1. Determine slug:
   - In #scratch or #general → propose 2-3 slug candidates, wait for user to create the channel.
   - Otherwise → slug = current channel name. Confirm before scaffolding.

2. Determine org per "Supabase orgs" section above.

3. Scaffold:
   - mkdir ~/projects/<slug> && cd ~/projects/<slug>
   - npx create-next-app@latest . --typescript --tailwind --app --no-eslint --use-npm --yes
   - npm install @supabase/supabase-js @supabase/ssr
   - npx shadcn@latest init -d

4. Init git + GitHub repo:
   - git add . && git commit -m "Initial scaffold"
   - gh repo create <slug> --public --source=. --remote=origin --push

5. Create Supabase project:
   - supabase projects create <slug> --org-id <org> --db-password $(openssl rand -base64 24) --region us-west-1
   - Save the project ref. Wait for ACTIVE_HEALTHY status (poll with `supabase projects list`).
   - Get URL and anon key: supabase projects api-keys --project-ref <ref>
   - Write to .env.local (NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY).
   - Confirm .env.local is in .gitignore.

6. Vercel:
   - vercel link --yes --token=$VERCEL_TOKEN
   - vercel env add NEXT_PUBLIC_SUPABASE_URL production --token=$VERCEL_TOKEN
   - vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY production --token=$VERCEL_TOKEN
   - vercel --prod --token=$VERCEL_TOKEN

7. Scaffold standard auth files:
   - lib/supabase/server.ts (createServerClient pattern using next/headers cookies)
   - lib/supabase/client.ts (createBrowserClient)
   - middleware.ts at project root (refreshes session)
   - app/auth/callback/route.ts (OAuth callback handler that exchanges code for session)
   - app/login/page.tsx (page with "Sign in with Google" shadcn button calling supabase.auth.signInWithOAuth)
   Use the latest @supabase/ssr docs as reference.

8. Commit auth + push.

9. Pause and ask user for Google OAuth credentials:
   "Project deployed but Google sign-in needs OAuth credentials.
    1. Open https://console.cloud.google.com/apis/credentials
    2. Create OAuth 2.0 Client ID (Web application)
    3. Authorized redirect URI: https://<supabase-ref>.supabase.co/auth/v1/callback
    4. Paste me the Client ID and Client Secret here."

10. After user pastes credentials, configure Supabase Auth Google provider via Management API:
    curl -X PATCH "https://api.supabase.com/v1/projects/<ref>/config/auth" \
      -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"external_google_enabled":true,"external_google_client_id":"<id>","external_google_secret":"<secret>"}'

11. Test: open the live Vercel URL, click "Sign in with Google", confirm the flow completes.

12. Final report in the channel:
    - <slug> ready
    - Path: ~/projects/<slug>
    - GitHub: <url>
    - Vercel: <url>
    - Supabase: <url>

## Port-in workflow (existing GitHub repo)

1. Confirm slug = channel name = repo name. Ask user if they don't match.
2. git clone <url> ~/projects/<slug>
3. Read package.json + README + next.config.* to identify stack.
4. If standard stack (Next.js + TS + Tailwind): npm install, vercel link --yes, set up channel context. If Supabase missing from env, ask if user wants it added.
5. If different stack: REPORT findings, ASK whether to (a) keep as-is, (b) migrate to standard, (c) something else. Default: (a).
6. Final report with "ported from <url>" header.

## Working in established project channels

When ~/projects/<channel-name>/ exists:
- cd there first, always
- "the project," "the code," "the app" = this project
- Default to standard-stack assumptions

## Subagents

Spawn for parallelizable work (audit all projects, add file to every project). Don't fan out for single-project tasks.

## Never without explicit user approval

- Push directly to main on a repo with users (use feature branches + gh pr create)
- Drop a Supabase table or run destructive SQL
- Delete a Vercel project or GitHub repo
- Modify Google OAuth settings (no CLI access anyway)
- rm -rf anything outside ~/projects/<current-slug>
- Spend money (paid Vercel/Supabase plans, paid services) — always ask first
STACKEOF
```

Patch in your real Supabase org IDs:

```bash
# Replace these with your actual org IDs from supabase orgs list
sed -i "s/PASTE_DEFAULT_ORG_ID/your-personal-org-id/" ~/projects/STACK.md
sed -i "s/PASTE_BUSINESS_ORG_ID/your-business-org-id/" ~/projects/STACK.md
```

(If you only have one org, paste it for both lines.)

## 10.2 — Append the workflow to Hermes's persistent prompt

```bash
cat >> ~/.hermes/SOUL.md << 'SOULEOF'

# Multi-project Discord workflow

Each Discord channel = one software project. Non-project channels: #general, #random, #scratch, #if-internal, #agent-logs.

For project channels:
- If ~/projects/<channel-name>/ exists → cd there, work on the existing project.
- If it doesn't exist → read ~/projects/STACK.md and follow the appropriate workflow (greenfield or port-in based on user's message).
- ALWAYS read STACK.md before scaffolding. Don't trust memory of it.

For #scratch and #general: help propose project names. Don't scaffold there.

After multi-step workflows (scaffold, port, deploy), post a final status message in the channel summarizing project location, GitHub URL, Vercel URL, Supabase URL, and any pending manual steps.
SOULEOF
```

## 10.3 — Update Discord channel prompts

`[Cursor]` Open `.hermes/config.yaml`. Find `channel_prompts: {}` under `discord:`. Replace with:

```yaml
  channel_prompts:
    general: "Not a project channel. Help the user navigate."
    random: "Casual chat only. Be brief."
    scratch: "Brainstorm and propose project names. Not a project channel itself."
    if-internal: "Internal company notes. Default to discussion mode."
    agent-logs: "Cron and sub-agent reports. Synthesize, don't chat."
```

Save (Cmd+S).

## 10.4 — Tune the noise level (optional but recommended)

In the same `config.yaml`, find `display:` section. Find:

```yaml
  tool_progress: all
```

Change to:

```yaml
  tool_progress: new
```

> **Why?** `all` shows every tool call inline in Discord, which gets noisy fast. `new` shows the tool name only when it changes (e.g., "now using terminal", instead of 47 individual terminal lines). `off` is silent — Hermes just gives final answers. `new` is the sweet spot.

Save.

## 10.5 — Restart the gateway

```bash
sudo systemctl restart hermes-gateway
```

Wait ~10 seconds.

## 10.6 — Test naming proposal

`[Discord, in #scratch]`:

```
i want to build a thing for tracking my guitar tabs - songs, keys, difficulty
```

Hermes should propose 2-3 slug candidates and tell you to create the channel.

## 10.7 — Test scaffolding

Pick one of the proposed names. `[Discord]` create that channel (e.g., `#tab-tracker`). Then in that channel:

```
build it
```

Watch Hermes work. The full scaffold takes 5-10 minutes:

1. Filesystem scaffold (Next.js, npm install)
2. Git init + GitHub repo
3. Supabase project (slow, 1-2 min)
4. Vercel link + env vars + deploy
5. Pause asking for Google OAuth credentials
6. After you paste credentials, finish auth wiring + test
7. Final summary

> **You'll see "Command Approval Required" prompts** for destructive operations like `rm -rf` (rare during scaffold) or for new commands the agent hasn't run before. Click **Allow Once** for one-off, **Allow Session** for "trust this for the current task," **Always Allow** for "trust this forever" (use sparingly). **Don't click "Always Allow" on `rm -rf`** — that's a permanent footgun.

> **If something goes wrong mid-scaffold and you need to clean up:**
> ```
> stop everything. clean up the <slug> project entirely — delete the local files at ~/projects/<slug>, the github repo, any vercel project, any supabase project. forget this project from your memory. confirm what you cleaned up when done.
> ```
> Hermes will prompt for approval on each delete; click Allow Session to speed it up.

> **Don't restart the gateway during a long-running task.** Half-completed scaffolds can leave orphaned Supabase projects, etc. Wait for the task to finish (or fail explicitly) before any `systemctl restart`.

---

# Part 11: Reboot test (5 min)

The real persistence proof. Confirms your full stack survives a reboot from a kernel update at 3am.

## 11.1 — Reboot

```bash
sudo reboot
```

Cursor will disconnect within seconds. **Wait 60 seconds** for the Droplet to fully restart.

## 11.2 — Test from your phone

`[Discord on phone, in #general]`:

```
you alive?
```

If you get a response within ~10 seconds, the entire stack auto-started successfully.

## 11.3 — Reconnect Cursor

Cmd+Shift+P → **Connect via SSH** → hermes. Or click the SSH indicator at the bottom-left when it offers reconnect.

File → Open Folder → `/home/hermes`.

## 11.4 — Health check

```bash
sudo systemctl status hermes-gateway   # active (running)
gh auth status                          # logged in
ls ~/.codex/auth.json ~/.hermes/.env    # both exist
tailscale status                        # connected
free -h                                 # check swap is active
```

All five healthy = setup is real and persistent.

---

# Part 12: First real use

You're done with setup. Now use the thing:

`[Telegram, on your phone]`:

```
What's on my plate today?
```

`[Discord, in #scratch]`:

```
Help me think through what's the most important thing I should be working on this week.
```

`[Discord, in any project channel]`:

```
remember: I prefer functional React components over class components, and I always use server components by default in Next.js
```

(Then in any future project, Hermes will apply this — cross-channel memory at work.)

---

# Things that will eventually break

## Codex OAuth expires (1-3 months)
- **Symptom:** replies start coming from `gpt-5.4` (the fallback model)
- **Check:** `journalctl -u hermes-gateway --since '1 hour ago' | grep -i 'auth\|401\|codex'`
- **Fix:** rerun the saved tunnel command:
  ```
  ssh -L 1455:localhost:1455 hermes@hermes
  codex login
  sudo systemctl restart hermes-gateway
  ```

## OpenRouter credit runs out
- Top up at openrouter.ai. Set a usage alert.

## Memory pressure during builds
- `free -h` while it's happening. If swap is fully used, resize the Droplet (DO console → Resize → power off, change size, power on; ~2 min, no data loss).

## Disk fills with `node_modules`
- 120 GB takes a while. Periodic cleanup: `du -sh ~/projects/*/node_modules | sort -h` to find heavy ones, `rm -rf` what you don't need — `npm install` rebuilds in seconds.

## Tailscale flakes
- Re-toggle the Tailscale app on your Mac. DO web console is your lifeline if Tailscale itself breaks (DO Console → Droplet → Console).

## Cursor Remote-SSH issues
- Cmd+Shift+P → **Kill Cursor Server on Host** → reconnect. Fixes 90% of weird Remote-SSH bugs.

## Another VPN turns on and breaks Tailscale routing
- `ping 100.x.x.x` (your Droplet's Tailnet IP) is the canary. If it stops working, check for a second VPN.

## Discord bot stops responding to plain messages after an update
- Re-verify `MESSAGE CONTENT INTENT` is still enabled at [discord.com/developers/applications](https://discord.com/developers/applications).

---

# Cost summary

- $32 Droplet (Premium Intel, 2 vCPU / 4 GB RAM / 120 GB SSD)
- $20 ChatGPT Plus (subsidizes GPT-5.5 via Codex OAuth)
- ~$5 OpenRouter buffer (fallback, lasts months)
- $0 GitHub, Discord, Telegram, Tailscale (all free tier sufficient)
- $0 Vercel, Supabase (free tier per-project, plenty for personal use)
- **Total: ~$57/mo** for a 24/7 personal AI agent with subsidized GPT-5.5, persistent memory, multi-channel reach, autonomous webapp building, and full reboot persistence

---

# What you've actually built

A self-hosted personal AI agent that:

- Lives on a private network invisible to the public internet
- Has persistent memory across all interactions
- Is reachable from anywhere via mobile messaging (Telegram) or desktop chat (Discord)
- Can autonomously scaffold full webapps with database, auth, and deployment
- Knows which project you're working on by which channel you're in
- Has a fallback model so it doesn't go silent when one provider hiccups
- Survives reboots, crashes, and most failure modes
- Costs less than a coffee a day to run

What's next is up to you. Try porting an existing project. Try setting up cron jobs. Try building something Hermes-shaped (an admin dashboard for itself, scheduled email digests, anything). The infrastructure is real now — just use it.

---

# Things this guide deliberately doesn't cover

These are real features but adding them upfront muddies the core setup. Come back to these after a week of real use:

- **AgentMail integration** — gives Hermes its own email identity at `*.agentmail.to` for newsletter signups, OTPs, agent-to-agent email
- **Hermes Hub dashboard** — a web UI for visualizing all current activity, sub-agents, memory state
- **Sub-agent fan-out** — spawning parallel agents for tasks like "audit all 5 projects for security issues"
- **Cron jobs** — scheduled tasks that run autonomously, like "every Monday at 9am, summarize last week's GitHub activity"
- **Tailscale on phone** — so you can SSH into the Droplet from your phone if something breaks while you're away
- **Daytona sandboxing** — running heavy builds in disposable cloud sandboxes instead of on the Droplet itself
- **Per-project channel prompts** with rich context about specific tech stacks per project

Each of these is a 5-15 minute addition once you have a clear use case. Build the foundation first, layer features as you discover what you actually want.