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
10 Powerful AI Business Applications to Boost Growth in 2025
Discover 10 powerful AI business applications that can transform your operations, boost efficiency, and drive growth. Learn how to implement AI today.
Read MoreP10 – 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 More

