P6 - Ultimate 9Router OpenClaw Setup
Step 0:Enable Codex in Chatgpt settings for accounts
Setting => Security => Enable device code author...
Step 1: Install Node/npm and 9router
node -v
npm -v
npm install -g 9router
If node/npm/git is not installed, then:
Install Node 22 LTS:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install nodejs -y
sudo apt install curl git -y
Step 2: Run 9Router
On the VM, execute the command:
9router
Dashboard (for Windows, open directly from this link): http://localhost:20128/dashboard
Since I'm using Ubuntu on a different machine, I will SSH into this Ubuntu AI machine from my machine.
Client: Open CMD and SSH into the AI VM.
ssh -N -L 20128:127.0.0.1:20128 bao@192.168.16.253 (IP VM AI)
Enter pass SSH
Step 3: Connect to the provider in the dashboard
Go to:
• Dashboard → Providers → select Codex / ChatGPT (Codex) (or “Codex”)
• Click Connect → perform OAuth/device login as instructed
It will show the localhost link, copy that link and paste it into the terminal/SSH on the AI machine.
Add the second account 2
Repeat “Connect” again (or “Add account”) to log in with the second account.
Similarly
enable round robin
Create combos : name openai
Step 4: Create the 9router API key
Name the key; it will generate the key automatically.
Key: sk-ccf64851645b8890-g9lfc4-4241aeb4
Test the key on the VM
KEY='sk-ccf64851645b8890-g9lfc4-4241aeb4'
curl -sS -H "Authorization: Bearer $KEY" http://127.0.0.1:20128/v1/models | head -c 400; echo
Step 5: Change the OpenClaw provider openai-codex to the 9Router endpoint
Run the batch command
openclaw config set --batch-json "[
{\"path\":\"models.mode\",\"value\":\"merge\"},
{\"path\":\"models.providers.openai-codex\",\"value\":{
\"baseUrl\":\"http://127.0.0.1:20128/v1\",
\"api\":\"openai-completions\",
\"headers\":{\"Authorization\":\"Bearer $KEY\"},
\"models\":[]
}}
]"
Restart service
systemctl --user restart openclaw-gateway
Validate + restart gateway
openclaw config validate --json
valid: true
systemctl --user restart openclaw-gateway
Verify OpenClaw to see the model via 9Router:
openclaw models status –plain
Edit the config file again:
nano ~/.openclaw/openclaw.json
Add /cx to the configuration file.
Save file: Ctrl O enter, Ctrl X exit
If you need to check the regenerated models file:
cat ~/.openclaw/agents/main/agent/models.json | head -n 60
Disabling round robin will automatically switch to the opanai1 account.
Step 6: Create a systemd user service.
#0: Enable CLI Tools Openclaw
#1 Create a unit file:
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/9router.service
Paste the following content (recommended to bind locally-only for safety):
[Unit]
Description=9Router (OpenAI-compatible router dashboard)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=PATH=%h/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WorkingDirectory=%h
ExecStart=%h/.npm-global/bin/9router --host 127.0.0.1 --port 20128 --no-browser --log --skip-update
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target
Save file
#2 Reload systemd user + enable + start
systemctl --user daemon-reload
systemctl --user enable --now 9router
#3 Make openclaw-gateway wait for 9router
mkdir -p ~/.config/systemd/user/openclaw-gateway.service.d
Paste the command exactly as is
cat > ~/.config/systemd/user/openclaw-gateway.service.d/override.conf <<'EOF'
[Unit]
Wants=9router.service
After=9router.service
EOF
Restart service
systemctl --user daemon-reload
systemctl --user restart 9router
systemctl --user restart openclaw-gateway
#4 Check status + view real-time logs
systemctl --user status 9router --no-pager -l
journalctl --user -u 9router -f
#5 Quick Management Commands
systemctl --user restart 9router
systemctl --user stop 9router
systemctl --user start 9router
5) Test live endpoint
curl -sS http://127.0.0.1:20128/v1/models | head -c 300; echo
6) (Important) Run automatically after reboot
User services sometimes only run when the user has a session. If you want the service to boot up and run automatically even without SSH access, you need to enable linger (requires sudo):
sudo loginctl enable-linger bao
Step 7: Set up a script to monitor quota by user account
1. Create a bin directory (if it doesn't exist)
mkdir -p ~/bin
2. Create a Python file
nano ~/bin/9router-account-usage.py
(Content: parse ~/.9router/log.txt, remove the PENDING line, add by account, print the column table)
#!/usr/bin/env python3
"""Summarize 9Router usage per account from ~/.9router/log.txt.
Expected log format (from 9Router):
DD-MM-YYYY HH:MM:SS | <model> | CODEX | <account> | <prompt_tokens or -> | <completion_tokens or -> | <status>
We count only completed (non-PENDING) rows.
"""
from __future__ import annotations
import argparse
import datetime as dt
import os
import re
import sys
from collections import defaultdict
LOG_PATH_DEFAULT = os.path.expanduser("~/.9router/log.txt")
LINE_RE = re.compile(
r"^(?P<date>\d{2}-\d{2}-\d{4})\s+(?P<time>\d{2}:\d{2}:\d{2})\s+\|\s+"
r"(?P<model>[^|]+?)\s+\|\s+"
r"(?P<provider>[^|]+?)\s+\|\s+"
r"(?P<account>[^|]+?)\s+\|\s+"
r"(?P<prompt>[^|]+?)\s+\|\s+"
r"(?P<completion>[^|]+?)\s+\|\s+"
r"(?P<status>.+?)\s*$"
)
def parse_int(s: str) -> int:
s = s.strip()
if s == "-" or s == "":
return 0
try:
return int(s)
except ValueError:
return 0
def parse_args() -> argparse.Namespace:
ap = argparse.ArgumentParser(description="9Router per-account usage table")
ap.add_argument("--log", default=LOG_PATH_DEFAULT, help=f"Log file path (default: {LOG_PATH_DEFAULT})")
ap.add_argument("--date", default="today", help="Filter date: YYYY-MM-DD or 'today' (default: today)")
ap.add_argument("--tz", default=None, help="Timezone name (best-effort). Default: local time on server")
ap.add_argument("--top", type=int, default=50, help="Max accounts to show (default: 50)")
return ap.parse_args()
def resolve_date(date_arg: str) -> dt.date:
if date_arg == "today":
return dt.date.today()
return dt.date.fromisoformat(date_arg)
def main() -> int:
args = parse_args()
path = os.path.expanduser(args.log)
if not os.path.exists(path):
print(f"Log not found: {path}", file=sys.stderr)
return 2
target_date = resolve_date(args.date)
# account -> metrics
m = defaultdict(lambda: {
"requests": 0,
"ok": 0,
"err": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"models": defaultdict(int),
"last": None,
})
with open(path, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.rstrip("\n")
if not line.strip():
continue
mo = LINE_RE.match(line)
if not mo:
continue
status = mo.group("status").strip()
if status.upper() == "PENDING":
continue
# parse date in log: DD-MM-YYYY
d_str = mo.group("date")
t_str = mo.group("time")
d = dt.datetime.strptime(d_str, "%d-%m-%Y").date()
if d != target_date:
continue
account = mo.group("account").strip()
model = mo.group("model").strip()
prompt = parse_int(mo.group("prompt"))
completion = parse_int(mo.group("completion"))
rec = m[account]
rec["requests"] += 1
if "200" in status or status.upper().startswith("OK") or "OK" in status.upper():
rec["ok"] += 1
else:
rec["err"] += 1
rec["prompt_tokens"] += prompt
rec["completion_tokens"] += completion
rec["models"][model] += 1
rec["last"] = f"{d_str} {t_str}"
rows = []
for acct, rec in m.items():
total_tokens = rec["prompt_tokens"] + rec["completion_tokens"]
top_models = ",".join([f"{k}:{v}" for k, v in sorted(rec["models"].items(), key=lambda kv: (-kv[1], kv[0]))[:3]])
rows.append((
acct,
rec["requests"],
rec["ok"],
rec["err"],
rec["prompt_tokens"],
rec["completion_tokens"],
total_tokens,
top_models,
rec["last"] or "-",
))
rows.sort(key=lambda r: (-r[1], r[0]))
rows = rows[: args.top]
# print table
header = [
"ACCOUNT",
"REQ",
"OK",
"ERR",
"PROMPT_TOK",
"COMP_TOK",
"TOTAL_TOK",
"TOP_MODELS",
"LAST",
]
# compute widths
widths = [len(h) for h in header]
for r in rows:
for i, val in enumerate(r):
widths[i] = max(widths[i], len(str(val)))
def fmt_row(vals):
return " ".join(str(v).ljust(widths[i]) for i, v in enumerate(vals))
print(fmt_row(header))
print(" ".join("-" * w for w in widths))
for r in rows:
print(fmt_row(r))
# totals
tot_req = sum(r[1] for r in rows)
tot_prompt = sum(r[4] for r in rows)
tot_comp = sum(r[5] for r in rows)
print()
print(f"TOTAL requests={tot_req} prompt_tokens={tot_prompt} completion_tokens={tot_comp} total_tokens={tot_prompt+tot_comp}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Save file: Ctrl O enter, Ctrl X exit
3. Permission
chmod +x ~/bin/9router-account-usage.py
4. Create a wrapper ~/bin/9router-account-usage for a shorter call.
cat > ~/bin/9router-account-usage <<'SH'
#!/usr/bin/env bash
set -euo pipefail
exec "$HOME/bin/9router-account-usage.py" "$@"
SH
Permission
chmod +x ~/bin/9router-account-usage
5. Test
~/bin/9router-account-usage --date today | head -n 60
~/bin/9router-account-usage --date today
See also related articles
P10 – Uninstall OpenClaw Windows Fast
P10 – Uninstall OpenClaw Windows Fast https://youtu.be/1ljEMzohiSY 🚀 AI Tutorial – P10: Uninstall OpenClaw on Windows (Clean Removal & Fix Issues) If you’re facing issues with OpenClaw or simply want to remove it completely, performing a proper Uninstall OpenClaw Windows process is essential. A partial uninstall may leave behind background...
Read MoreP9 – Build Local AI Telegram Bot Fast (Ollama Guide)
P9 – Build Local AI Telegram Bot Fast (Ollama Guide) https://youtu.be/YuiLJDLIVr0 🚀 AI Tutorial – P9: Create a Local AI Telegram Bot with Ollama in Minutes Building a Local AI Telegram Bot is one of the fastest ways to bring AI into real-world usage. By combining Ollama with a simple...
Read MoreP8 – Ultimate OpenClaw Local AI Setup
P8 – Ultimate OpenClaw Local AI Setup 🚀 AI Tutorial – P8: Complete Guide to Running Local AI with Ollama, Qwen & Open WebUI Running openclaw local AI is one of the most powerful ways to build a private, fast, and cost-efficient AI system. By combining Ollama, Qwen models, and...
Read More