Turn on the dashboard
Add two lines to your config, or run one command with no credentials. The setting, the ports, the container, and the demo.
On this page
The dashboard is part of the binary, so turning it on is a setting rather than an install. This page covers the setting, the two ports it interacts with, the container, and the no-credentials path if you want to look before you commit.
See it first, with no configuration
tokentriage demo boots the proxy and the dashboard against a synthetic ledger, points the
proxy at built-in mock upstreams, and drives a trickle of requests through the pricing
pipeline so rows arrive while you watch. No API key is involved, and the temp directory is
removed when you stop it.
$ tokentriage demo
demo: temp ledger at D:\tt-tmp\tokentriage-demo-35440665, removed on exit
tokentriage dev · mode: ledger
DEMO — synthetic data (watermarked)
proxy: http://127.0.0.1:4215 (point your app's base_url here)
dashboard: http://127.0.0.1:4216/ui/
ledger: D:\tt-tmp\tokentriage-demo-35440665\decisions.jsonl
next:
$env:ANTHROPIC_BASE_URL = "http://127.0.0.1:4215"
tokentriage wrap -- <your app> (Anthropic clients; no env changes)
tokentriage doctor (check the setup)
Three things in that capture are the machine’s rather than yours. It came from a source
build, so a release prints its version where this says dev. It ran on Windows with a
redirected temp directory, so your paths and your shell’s export line will differ. And the
ports are the ones this run happened to get.
Its ledger is synthetic and every dollar tile carries a diagonal DEMO DATA watermark, so nothing you see there can be mistaken for your own traffic. It writes to a temp directory and does not touch a ledger you already have.
Turn it on in a config file
Two lines:
dashboard:
enabled: true # default: false
Then validate and start. Validation decodes the block strictly, so an unknown key under
dashboard: is an error rather than a setting that quietly does nothing:
$ tokentriage validate --config tokentriage.yaml
mode: ledger
listen: 127.0.0.1:8787 admin: 127.0.0.1:9090
upstreams: 1
- openai openai https://api.openai.com
tiers: 1 (cheapest -> most capable)
0. frontier -> openai/gpt-4o-mini
rules: 0
OK: configuration is valid
The boot banner prints the dashboard URL when the admin listener binds:
$ tokentriage run --config tokentriage.yaml
tokentriage dev · mode: ledger
proxy: http://127.0.0.1:8787 (point your app's base_url here)
dashboard: http://127.0.0.1:9090/ui/
ledger: D:/tt-tmp/ttdocs/state/decisions.jsonl
next:
$env:ANTHROPIC_BASE_URL = "http://127.0.0.1:8787"
tokentriage wrap -- <your app> (Anthropic clients; no env changes)
tokentriage doctor (check the setup)
tokentriage demo (no traffic yet? the whole journey on synthetic data)
The ledger path is the one this config named; yours is whatever ledger.decision_log.path
says, defaulting to $HOME/.tokentriage/decisions.jsonl.
Give it its own index path
The dashboard keeps a SQLite read model derived from your ledger. Its path defaults to
~/.tokentriage/index/decisions.sqlite, which is shared by every instance that does not set
one:
dashboard:
enabled: true
index:
path: /var/lib/tokentriage/index/decisions.sqlite
Set it per instance. Two instances with different ledgers and one default index path will
contend over the same file, and a large or unexpected index at that path can stall a boot
while it is opened and caught up before the listener comes up. If a start hangs after
replica identity resolved and before the banner, that is where it is.
The read model is derived and disposable — deleting it costs you nothing but the rebuild.
The read model covers when to do that and what index rebuild does.
The two ports
The dashboard is served from the admin listener, never the data plane.
| Default | Serves | |
|---|---|---|
| Data plane | 127.0.0.1:8787 |
the proxy (server.listen) |
| Admin plane | 127.0.0.1:9090 |
/ui/, /tt/api/v1, /health, /metrics (server.admin) |
Both default to loopback. That default is what makes auth.mode: off valid out of the box:
a non-loopback admin bind with authentication off is a validation error rather than a warning.
Before you move the admin listener off loopback, read sign in to the
dashboard and Operate’s hardening
guide, which owns the network stance for the admin plane.
An admin-plane bind failure is not fatal. The proxy keeps serving and the banner says so:
dashboard unavailable: listen tcp 127.0.0.1:9090: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted. — proxy traffic unaffected (run: tokentriage doctor)
tokentriage dev · mode: ledger
proxy: http://127.0.0.1:8787 (point your app's base_url here)
The sentence in the middle is the operating system’s, so Linux and macOS say “address
already in use” where that Windows capture says something longer. What matters is the part
after it, and the missing dashboard: line below.
A data-plane bind failure, by contrast, stops the process. The asymmetry is intentional: the proxy is the product, and losing the read plane should not take traffic down with it.
In a container
Two facts about the published image surprise people, and they compound.
The baked-in config has no dashboard: block, so the image serves no UI. Port 9090 is
published and answers /health and /metrics, but /ui/ is not there until you mount a
config that turns it on. Install TokenTriage covers the
image itself and the /data volume it writes to.
A container config binds 0.0.0.0, and that changes which authentication modes are legal.
A loopback bind inside a container is unreachable from outside it, so the admin listener must
bind a routable address, and an unauthenticated dashboard on one is rejected at validation
rather than warned about.
The repository’s Compose profiles do this for you. deploy/compose/compose.demo.yaml brings up
a token-authenticated demo dashboard against an in-network mock upstream with no provider key
on the machine, and its config is the worked example of the two facts above. The UI shell and
/tt/api/v1/capabilities are served before authentication so the demo label is visible without
a token; reading any data needs one.
Publishing 9090 to a host interface publishes the admin plane. Keep it on an internal network or behind your own proxy; production deployment owns the topology and hardening owns the network stance.
Everything under `dashboard:` and its default
Every field below is optional. The values are the ones a two-line block resolves to, read
from GET /config/running on a running instance.
| Key | Default | What it does |
|---|---|---|
enabled |
false |
serve /ui/ and /tt/api/v1 on the admin listener |
auth.mode |
off |
off, token or users |
auth.session_ttl |
12h |
how long a minted session lasts |
state_dir |
~/.tokentriage/dash |
users, token hashes, saved views, audit log, event ring |
index.path |
~/.tokentriage/index/decisions.sqlite |
the read model |
stream.client_queue |
1024 |
per-client bounded queue on the live stream |
stream.max_clients |
32 |
live-stream client cap |
evidence.max_age |
168h |
how fresh shadow evidence must be to satisfy the promote gate |
query.deadline |
10s |
per-query time budget |
query.row_cap |
1000 |
maximum rows a single query returns |
query.max_concurrent |
2 |
concurrent heavy queries |
capture.request |
false |
store request bodies |
capture.response |
false |
store response bodies |
capture.ttl |
720h |
how long captured bodies survive |
capture.path |
~/.tokentriage/content |
where captured bodies live |
api_limits.read_per_min |
600 |
management-API read budget per principal |
api_limits.write_per_min |
120 |
management-API write budget per principal |
docs_public |
false |
serve the API reference without authentication |
demo |
false |
synthetic watermarked mode; tokentriage demo sets it |
index.enabled also decodes, and today it does not do what its name says: the read model is
opened whenever the dashboard is composed, and setting it to false neither skips the index
nor changes the capability the server advertises. Measured on a run --config instance with
index.enabled: false, which still created the SQLite file and still reported the read model
as available. Treat the read model as always on when the dashboard is on. This is filed.
What can go wrong
The banner prints no dashboard line. Either dashboard.enabled is false, or the admin
listener could not bind. Both cases print a reason; tokentriage doctor reports the state
without stopping anything.
Validation rejects auth.mode: off. The admin listener is not on loopback. That is the
container case above, and the fix is to pick an authentication mode rather than to move the
bind back.
The process starts but /ui/ shows “UI not built”. The binary was built from source
without the web assets. Release binaries embed them; a source build needs make ui before
go build.
A start hangs before the banner. Look at the index path, as above.