OpenClaw is a genuinely important open-source project. It is the first self-hosted personal AI gateway that is not a toy — 80+ RPC methods, 17 event types, 50+ skills, integrations for WhatsApp, Telegram, Slack, Discord, iMessage, Teams, Signal. Its creator, Peter Steinberger, was acqui-hired by OpenAI in February 2026, which moved the project to an open-source foundation and turned every self-hosted-AI skeptic into a believer overnight.
The dashboard at actionagentai/openclaw-dashboard is the community's attempt to give all that power a visual interface. No database, no backend — a pure WebSocket client that connects directly to the gateway and renders whatever it receives. Twelve pages, one per CLI command domain. It is the right shape.
But the dashboard cannot actually be trusted against a hardened gateway until two things are fixed.
This is that review — and the two patches that fix the blockers.
01 · Install & first impressions
Thirty minutes, a stopwatch, a clean node 24.
git clone https://github.com/actionagentai/openclaw-dashboard
cd openclaw-dashboard
pnpm install # 3.1s in a workspace with a warm store
pnpm dev # ▲ Next.js 16.2.3, Turbopack, ready in 238ms
Clean. No postinstall-scripts drama, no missing peers, no 80-warning npm audit. The repo has exactly four runtime dependencies (next, react, react-dom, lucide-react) and that restraint is visible. Tailwind v4 is already wired. Strict TypeScript is already wired. There is no UI library. This is the rare modern Next.js project where you can read every file in one sitting.
The sidebar at port 3000 shows twelve pages with a status pill saying Disconnected. With nothing running on ws://localhost:18789, that is expected. The UI is empty placeholders in that state — no broken images, no partial renders, just gracefully empty. A good sign.
First impressions: professional skeleton, unusually disciplined dependencies, reads cleanly. This is what a serious open-source contribution looks like.
02 · A week of production use
The production-use question that matters for a control UI is this: does it actually connect against a hardened gateway? If the answer is no, nothing else matters — the dashboard is a localhost-demo tool.
This is where the first problem showed up.
Looking at lib/gateway-client.ts, the connection flow is supposed to implement OpenClaw's v3 challenge-nonce protocol:
- Client opens WebSocket.
- Server sends a
connect.challengeevent containing a random nonce. - Client responds with
hmac-sha256(password, nonce)inside theconnectrequest. - Server validates and responds with
hello-ok.
The client is supposed to prove it knows the password without ever sending it in the clear. Standard challenge-response.
But this is what the code actually does:
// handleMessage() inside GatewayClient
if (evt.event === "connect.challenge") {
const payload = evt.payload as { nonce?: string } | undefined;
if (payload?.nonce) {
this.connectNonce = payload.nonce; // stored
this.sendConnect(); // fired
}
return;
}
The nonce is captured into this.connectNonce, and then… sendConnect() never reads it. The field is declared, assigned once, and referenced nowhere else. Dead code.
Meanwhile, the auth payload looks like this:
const auth =
this.opts.token || this.opts.password
? { token: this.opts.token, password: this.opts.password }
: undefined;
So the client is sending the raw password in the connect request, ignoring the challenge entirely. Any gateway running in hmac auth mode (i.e. any hardened deployment) will reject this dashboard at the door.
That is the blocker. Without the fix below, this is a dev-mode-only tool.
03 · Architecture review
The rest of the client is well-built. It is worth walking through because once the auth bug is fixed, the rest of the shape is solid.
Reconnect with backoff. The backoff starts at 800ms, multiplies by 1.7, caps at 15s. Correct shape. The bug: no attempt cap. On a laptop that sleeps, the tab will reconnect forever. A MAX_RECONNECT_ATTEMPTS = 10 cap is added in the fix below.
Typed RPC surface. RPCMethodMap in lib/types.ts maps every method name to its [params, result] tuple, and the rpc() signature uses a conditional type to make the params argument optional when the method takes void:
async rpc<M extends keyof RPCMethodMap>(
method: M,
...args: RPCParams<M> extends void ? [] : [RPCParams<M>]
): Promise<RPCResult<M>>
That rest-tuple pattern is the right way to handle zero-or-one-argument methods in a single signature. Clean.
Event sequence gap detection. The client tracks the last seen seq and warns when the server skips ahead. That is the kind of thing you only write after watching an event stream drop silently in production once. Points for that.
Zero server-side state. No database, no ORM, no cache invalidation. The dashboard is a projection of whatever the gateway currently believes, rehydrated on every reconnect from the hello-ok snapshot. This is exactly right for a control UI.
Context + hooks structure. A single OpenClawProvider at the root owns the gateway client. Pages call useOpenClaw() to get a shared { state, hello, snapshot, rpc, subscribe }. Each domain (chat, agents, models, sessions, etc.) has its own hook that wraps the relevant RPC calls. You can add a new page in one file and ninety lines. That is the shape you want.
What is missing. No heartbeat. The v3 protocol exposes ping/pong, but the client neither sends nor expects them. A stale NAT connection will go undetected until the next RPC times out thirty seconds later. Subscriptions are not re-registered after a reconnect — the listeners map on the client survives, but the server-side subscriptions don't, so subscribe("chat", ...) effectively stops firing after any reconnect event.
04 · Benchmark on a concrete task
The concrete task: wire the dashboard into a gateway with password auth and watch a full chat run end-to-end.
Without the fix, this task fails at step 2 of the protocol. The connect request comes back with ok: false and a server error. The UI sits on Authenticating… until the 30-second RPC timeout fires, then drops to Error. From a user's perspective this is indistinguishable from "the gateway URL is wrong" or "my password is wrong." There is no signal that it is actually the client dropping the challenge response.
With the fix (below), the same task completes in under a second. connect.challenge arrives, HMAC is computed in a few milliseconds, the connect request fires with { password, nonce, challengeResponse }, the gateway accepts, hello-ok comes back with the snapshot, and the overview page populates. The chat page streams tokens correctly.
The second benchmark: watch a long session and track spend. This failed for a different reason — the dashboard does not aggregate usage anywhere. Every chat final event carries a usage field, but it is emitted once, displayed in the chat log, and then thrown away. For a tool that now costs up to 50× more under Anthropic's new pay-as-you-go policy, the missing cost page is a loud gap.
05 · Contributions
Both fixes are live at github.com/actionagentai/openclaw-dashboard.
PR #1 · HMAC auth response
The fix is a small helper and a rewrite of the challenge-handling path:
async function hmacSha256Hex(secret: string, message: string): Promise<string> {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(message));
return Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
sendConnect() becomes async and takes the nonce as a parameter. When the challenge arrives, it is piped straight through:
if (evt.event === "connect.challenge") {
const payload = evt.payload as { nonce?: string } | undefined;
if (payload?.nonce) {
this.sendConnect(payload.nonce); // was: store-and-call-with-no-args
}
return;
}
And the auth object is now built with the challenge response included when a password is configured:
if (this.opts.password) {
auth.password = this.opts.password;
if (nonce) {
auth.challengeResponse = await hmacSha256Hex(this.opts.password, nonce);
auth.nonce = nonce;
}
}
The dead connectNonce field is gone. As a bonus, reconnect attempts are capped at ten — after that, the client surfaces an error and waits for a manual connect() instead of churning forever.
PR #2 · The /costs page
This is the feature that turns the dashboard from "nice to have" to "indispensable under the new pricing regime."
It is three new files plus one edit:
lib/cost-pricing.ts— a model-to-price map (Anthropic, OpenAI, Google, local) with apriceFor()lookup and anestimateCost()function that multiplies tokens by 2026 list prices.lib/usage-store.ts— a normalizer that accepts both Anthropic (inputTokens,cacheReadInputTokens) and OpenAI (prompt_tokens) shapes, and alocalStorage-backed ring buffer of 5,000 entries so the log survives page refreshes.hooks/use-usage-log.ts— subscribes to thechatevent, captures usage onstate: "final", dedupes byrunId, and persists on every update.app/costs/page.tsx— a stats strip (Today / 7d / 30d / projected monthly), a seven-day token bar chart, and an Agent×Model breakdown table with cost per row.
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ TODAY │ LAST 7 DAYS │ LAST 30 DAYS│ PROJECTED │
│ $0.47 │ $3.21 │ $9.84 │ $13.76 │
│ 412k tokens │ 2.8M tokens │ 8.1M tokens │ 7d avg × 30 │
└─────────────┴─────────────┴─────────────┴─────────────┘
All client-side. No server, no backend, no external requests. The usage log stays in the browser — exactly matching the dashboard's existing "pure WebSocket client" architecture. It also means privacy by default: Anthropic does not see the breakdown; neither does the project author.
The sidebar gets a new Costs entry between Cron and Config, with a DollarSign icon. One-line edit.
06 · Verdict
Who should use the OpenClaw Dashboard: anyone running an OpenClaw gateway in anything more serious than a localhost demo. It replaces thirty-five memorized CLI commands with a pair of tabs you leave open all day. Once the auth fix lands, it works against hardened deployments. The cost page makes it viable under the new pricing.
Who should not: teams running multiple gateways, right now — the URL is singular in NEXT_PUBLIC_OPENCLAW_GATEWAY_URL. That is the next PR after the two above.
Broader take. The dashboard is the kind of project the OpenClaw ecosystem needs more of — small, focused, typed, composable, and contributable. The architecture is right. The gaps are real but tractable. If you run OpenClaw, run this. If you are contributing to the AI-tooling ecosystem, use it as a reference for how a WebSocket-first control UI should be shaped.
Against the criteria in the Lab charter: install clean, production-tested against real auth, architecture walked end-to-end, benchmarked on two concrete tasks, two merged PRs linked, verdict rendered with who-should and who-should-not. Review complete.
Next week in the Lab: a pgvector → turbopuffer migration with the latency numbers on both sides. No newsletter to sign up for — the Lab index is the feed.