A mobile-first read-only dashboard for 9router — the AI Router & Token Saver. Built as a Telegram Mini App on top of Hermes Agent.
It reads 9router's local SQLite database directly (read-only), so it works without the dashboard password, has no session expiry, and never touches 9router's runtime. No tokens or secrets are ever exposed — API keys are masked, raw OAuth/refresh tokens and client secrets stay server-side.
- ⚡ Overview — requests, cost, and tokens today, lifetime requests, provider health, RTK / proxy feature state
- 🔌 Providers — connection status per account (Kiro, Xiaomi MiMo, etc.), auth type, OAuth token expiry, last error, last-used time
- 📊 Usage — breakdown by model and by provider, sorted by cost (requests, in/out tokens, cost)
- 📜 Logs — 50 most recent requests, tap to expand for latency, tokens, cost, endpoint, and error details
- ⚙️ Settings — API base URL config + backend health check
If you use an AI coding agent, paste this prompt and it handles everything (clone → deps → secure password → build → run → CF tunnel → hardening):
Install 9router-miniapp from https://github.com/lukmanc405/9router-miniapp
Read SKILL.md in the repo root first and follow it step by step.
9router is already running on localhost:20128. Set a strong APP_PASSWORD,
build, start the service, and apply the Step 7 security hardening.
For Hermes Agent specifically: skill_view(name='9router-miniapp-deploy')
The agent reads SKILL.md (full playbook: prerequisites, 9 install steps, CF tunnel + IP/API hardening, security limitations, verification). See AI Agent Installation at the bottom for details.
npm create 9router-miniapp@latest my-dashboard
cd my-dashboard
npm run build
cd server && node index.jsAuto-generates secure APP_PASSWORD, installs all deps, prints next steps. Requires repo to be public.
npx degit lukmanc405/9router-miniapp my-dashboard
cd my-dashboard
npm install && cd server && npm install && cd ..
cp .env.example .env
# Edit .env → set APP_PASSWORD
npm run build && cd server && node index.jsgit clone https://github.com/lukmanc405/9router-miniapp.git
cd 9router-miniapp
npm install && cd server && npm install && cd ..
cp .env.example .env
# Edit .env → set APP_PASSWORD
npm run build && cd server && node index.jsClick "Use this template" button on the GitHub repo page → creates a new repo under your account with the full source.
After install: open
http://localhost:9122, log in with yourAPP_PASSWORD. For production deployment + security hardening, readSKILL.mdor paste the AI Agent prompt above.
# 1. Clone
git clone https://github.com/lukmanc405/9router-miniapp.git
cd 9router-miniapp
# 2. Install dependencies
npm install
cd server && npm install && cd ..
# 3. Configure environment
cp .env.example .env
# Edit .env — set NROUTER_DB to your 9router SQLite path (default: ~/.9router/db/data.sqlite)
# 4. Build the frontend
npm run build
# 5. Start the server
cd server && node index.jsOpen http://localhost:9122 to preview.
├── src/ # React 19 + Vite + TypeScript frontend
│ ├── config.ts # App name, version, API base URL
│ ├── types.ts # Shared TS types (mirror server responses)
│ ├── App.tsx # Tab nav (Overview / Providers / Usage / Logs / Settings)
│ ├── pages/
│ │ ├── Home.tsx # Overview — daily stats + provider health
│ │ ├── Providers.tsx # Per-connection status + errors
│ │ ├── Models.tsx # Usage breakdown (models / providers toggle)
│ │ ├── Logs.tsx # Recent request logs (expandable)
│ │ └── Settings.tsx # API config + backend health
│ └── lib/api.ts # Fetch wrapper
├── server/
│ ├── index.js # Express API + static file serving
│ └── lib/nrouter.js # Read-only 9router SQLite data layer (sanitized)
└── .env.example
All under /api/ (Caddy handle_path /app/* strips the /app prefix before forwarding).
| Endpoint | Returns |
|---|---|
GET /api/health |
Server + DB health |
GET /api/overview |
Aggregate stats for Overview |
GET /api/providers |
Provider connections (sanitized) |
GET /api/breakdown/models |
Today's usage per model |
GET /api/breakdown/providers |
Today's usage per provider |
GET /api/usage/today |
Raw daily usage |
GET /api/usage/:date |
Usage for a specific day (YYYY-MM-DD) |
GET /api/logs?limit=50 |
Recent request logs |
GET /api/keys |
API keys (masked) |
GET /api/combos |
Model combo groups |
GET /api/settings |
Sanitized settings (no password hash) |
| Variable | Default | Description |
|---|---|---|
PORT |
9122 |
Server port |
CORS_ORIGIN |
* |
Allowed CORS origin |
NROUTER_DB |
~/.9router/db/data.sqlite |
Path to 9router's SQLite DB (read-only) |
NROUTER_URL |
http://localhost:20128 |
9router HTTP API base URL |
APP_PASSWORD |
(none) | Login password. If unset, dashboard is open (dev mode) |
MINIAPP_BOT_TOKEN |
(none) | Telegram Bot token for Mini App initData auth (optional) |
The app generates runtime secrets automatically on first start. These are stored in ~/.9router/ and never committed to git:
| File | Purpose |
|---|---|
~/.9router/miniapp-session-secret |
HMAC key for session cookies (auto-generated) |
~/.9router/miniapp-webauthn.json |
WebAuthn credential store (created on first passkey registration) |
~/.9router/jwt-secret |
JWT signing key for 9router API proxy auth |
To set up authentication:
# 1. Set a password in .env
echo "APP_PASSWORD=your-secure-password" >> .env
# 2. (Optional) Create a .PASSWORD file instead
echo "your-secure-password" > .PASSWORD
chmod 600 .PASSWORD
# 3. Start the server — session secret is auto-generated on first run
cd server && node index.jsOnce logged in with the password, you can register a WebAuthn passkey (Face ID / Touch ID / security key) from the app UI for passwordless login on subsequent visits.
- The database is opened read-only (
better-sqlite3withreadonly: true). - The data layer in
server/lib/nrouter.jssanitizes every response:- API keys → masked preview (
tp-s…uu95) - OAuth access/refresh tokens → never returned
- Client secrets → never returned
- Dashboard password hash → never returned
- API keys → masked preview (
- This is a monitoring dashboard. It does not write to 9router or proxy any AI requests.
Two app-level hardening gaps exist. Neither leaks secrets, and both are fully mitigated by fronting the app with Cloudflare Access + firewall (see SKILL.md Step 7), but you must address them before exposing publicly:
1. No rate limiting on /api/login — the login endpoint accepts unlimited attempts. If the URL leaks without edge protection, APP_PASSWORD can be brute-forced. Mitigate with Cloudflare Access, a CF WAF rate rule (5 req/min/IP), or an app-level limiter. Always use a strong password (openssl rand -base64 24).
2. CORS wildcard + credentials — default CORS_ORIGIN=* with credentials: true is a CSRF risk in production. Set CORS_ORIGIN to your exact HTTPS domain before deploying:
sed -i "s|^CORS_ORIGIN=.*|CORS_ORIGIN=https://9router.yourdomain.com|" .envLeave * only for local dev. Session cookies are SameSite=Lax which already blocks most CSRF.
Full mitigation steps and copy-paste code are in
SKILL.md→ Known Security Limitations.
cloudflared tunnel --url http://localhost:9122your-domain.com {
handle_path /app/* {
reverse_proxy 127.0.0.1:9122
}
}- Open @BotFather
- Your bot → Bot Settings → Menu Button → Configure
- Set URL to your deployed HTTPS URL
MIT
If you're using an AI coding agent (Hermes, Cursor, Claude, etc.), load the SKILL.md file in this repo root before starting. It contains the full step-by-step installation, configuration, security setup, and deployment procedure so the agent can install and configure the miniapp end-to-end without missing steps.
# For Hermes Agent:
skill_view(name='9router-miniapp-deploy')
# Or just point your agent to:
cat SKILL.md