TIER — Quickstart
v0.7.0 identity, coaching and retention: Real-data developer mode uses permitted viewer credentials, a nonempty admin token and a named audited operator. Named coaching-card reads use verified spend and log each view before construction. Shared reads remain name-free; raw-data retention runs at startup and daily. See developer access and upload identity for current commands and restrictions.
Audience: anyone who wants to run TIER against a repository for the first time.
Version scope: installation examples target v0.7.0, pending publication. Developer access, coaching and retention below describe this release; the synthetic demo remains isolated from real data.
Who this release is for (v0.7.0). Run one TIER server for one organization. Administrators and host/database owners remain trusted with retained developer data. Give each developer a bound upload credential and use scoped organization keys for administration and sealed subtree reports. Named coaching uses the declared-viewer boundary described below; shared/admin uploads remain team-only. Each person can use tierd score for their own local cost table or tierd score --outcomes for their own GitHub-token-bound personal TIER, with no persisted report or team band.
Access and trust (v0.7.0). Real-data developer mode requires subject-bound viewer credentials, a nonempty admin token and a named audited operator. Each developer ships with their own bound credential. Shared read credentials expose only name-free responses; shared/admin uploads remain team-only. Administrators and host/database owners remain trusted operators, and host access can read the whole database. Team/division modes remain name-free. The coaching card (#1372) ships with raw-data retention (#1371) in v0.7.0; the separate per-issue report remains future work. Card reads use verified spend and log each view before construction. The synthetic tierd demo keeps its startup exemption and invented data; tierd score remains a local cost report, with personal TIER available through --outcomes.
The k floor depends on counted identity evidence. Shared ingest no longer supplies that evidence; bound uploads and other qualifying sources do. Administrators can still pad a roster and provide qualifying evidence for invented people, so k does not protect against a malicious administrator. See the identity and census rules.
Table of contents
- Install
- See it work in one command
- Point it at your own repository
- The free cost view
- The full TIER score
- Capturing OpenAI Codex spend
- Viewing the dashboard from another machine
- Running on a different port
- What each command actually needs
1. Install
Building from source at this revision or later requires Go 1.27.3 or newer (v0.7.0; raised for GO-2026-6809); v0.6.1 shipped with Go 1.27.2. Download a release archive below if you do not have a Go toolchain.
go install github.com/tiermetric/tier/cmd/tierd@latest
@latest resolves to the newest tagged release — it does not track main,
so you always get a published, reproducible version. Pin an exact tag instead
(@v0.7.0, the release used as the example below) when you need a build that never moves, e.g. in CI.
go install puts the binary in $(go env GOPATH)/bin — usually ~/go/bin —
or in GOBIN if you have set it. If the next command says command not found,
add that folder to your PATH (e.g. export PATH="$HOME/go/bin:$PATH" in your
shell profile) and open a new shell.
Or download a signed release archive from
github.com/tiermetric/tier/releases.
Each release carries one archive per platform, named
tierd-<version>-<os>-<arch>.tar.gz: darwin-arm64 (Apple Silicon Mac),
darwin-amd64 (Intel Mac), linux-amd64, linux-arm64, and windows-amd64,
each with a .sha256 checksum beside it. The archive unpacks to a folder of the
same name holding a single file, tierd (tierd.exe on Windows); move it
somewhere on your PATH. <version> is the newest tag on the Releases page —
v0.7.0 is the target release used below; these archives become available when it is published:
tar -xzf tierd-v0.7.0-darwin-arm64.tar.gz
sudo mv tierd-v0.7.0-darwin-arm64/tierd /usr/local/bin/
# optional: gh attestation verify tierd-v0.7.0-darwin-arm64.tar.gz --repo tiermetric/tier
Homebrew (macOS): brew install tiermetric/tap/tierd builds tierd from source.
macOS: if you downloaded the archive with a web browser, macOS marks the
binary as downloaded, and because it is not notarized the system stops it from
running. Remove that mark once (if it says No such xattr, there was no mark
and nothing to do):
sudo xattr -d com.apple.quarantine /usr/local/bin/tierd
Windows: a windows-amd64 archive is published. A native command-line smoke
test on Windows 11 Enterprise x64 (2026-10-22, synthetic data only) passed:
download and checksum, tierd version, tierd demo, tierd serve, and
tierd score on empty history, all on v0.6.2. Not yet tested there: scoring
populated data, the dashboard against a real server, a browser download of the
unsigned binary (SmartScreen), and this page's bash/zsh commands in PowerShell
or cmd; there is no Windows CI. The 0600 guarantee does not apply
(security.md §4): under your user profile,
SYSTEM, Administrators and your account had full control; another folder gives
its own permissions, and other accounts' access was not tested. Treat Windows as
experimental.
Confirm the build:
tierd version # also: tierd -version / --version / -v
2. See it work in one command
No configuration, no database, no repository — synthetic data:
tierd demo # then open http://127.0.0.1:8080
Everything on that dashboard is invented (developers named demo-*, issues
DEMO-*). It is the fastest way to see what TIER produces before pointing it at
anything real.
3. Point it at your own repository
Before measuring anything, check that capture works on your machine:
cd ~/src/your-repo
tierd doctor --repo .
doctor reports what it can and cannot capture. A common first result is an
attribution FAIL — TIER telling you that a large share of your AI spend
cannot be tied to any issue. That is the tool working, not breaking: name
branches feature/<issue-number>-slug so cost can be linked to an outcome, or
lower --min-attribution if partial coverage is acceptable for this install.
4. The free cost view
tierd score --repo . --since 2026-05-01
From v0.7.0, this reads your own local Claude Code, Codex CLI, Opencode (Z.ai
coding plan only) and Muse Code logs and prints cost per issue, per-source
subtotals and the total — no server, no token, no GitHub access. Defaults are
~/.claude/projects/, ~/.codex/sessions/,
~/.local/share/opencode/opencode.db (opened read-only) and
~/.local/share/muse/sessions/. Override them with --claude-dir <dir>,
--codex-sessions-dir <sessions-dir>, --opencode-db <file> and
--muse-home <muse-home> (reads its sessions/ subdirectory). Missing sources
are skipped with a stderr note. Opencode is opened read-only; SQLite may leave
its -shm/-wal files beside it; nothing else is written. The “issue-attributed share of local AI spend
(no outcome join)” is issue-linked spend divided by all captured spend,
including every unattributed bucket in the denominator. With no spend the share is unavailable. The caveat names the server's real ranking requirements (at least 3 outcomes, at least $5 spend, zero flagged outcomes); there is no attributed-spend percentage ranking threshold.
For your own local TIER, add --outcomes:
# Set TIER_GITHUB_TOKEN privately, or add --token @/path/to/token.
tierd score --repo . --since 2026-05-01 --outcomes
This joins only the token owner’s merged PRs (GET /user) to your local costs,
using the server’s scoring window and data-quality flags. Your OS username is
aliased to that login for this run only. No team band is shown. Nothing persists:
the private temporary SQLite directory is removed on exit, errors and Ctrl-C.
HTTPS is required. Literal token flags, identity overrides, --db, --server
and shipping flags are refused. On a fork, use --repo-slug upstream-owner/name.
Costs from all four sources join only the token owner’s own merged PRs.
Historical PRs start at quality 1.0; later
CI failures and reverts are not reconstructed. Each outcomes run prints:
TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.
5. The full TIER score
A TIER score divides merged work by AI spend, so it needs both loaded. Four steps: load your merged work, start the server, load the spend already on your disk, and join your two names if they differ.
# 1. Reconstruct outcomes from merged-PR history (do this BEFORE serve —
# SQLite is single-writer). Needs a GitHub token that can read this repo's
# pull requests: a fine-grained token with "Pull requests: Read-only" on
# the one repo, saved to a file. Steps: README → "What you need before you
# start" (https://github.com/tiermetric/tier#what-you-need-before-you-start).
tierd backfill --repo owner/name --token @$HOME/.tier/github-token
# 2. Run the server: it captures new spend and serves the dashboard.
# Leave it running. It does NOT record PRs merged from now on: it sets
# no webhook secret (no --webhook-secret flag, TIER_WEBHOOK_SECRET or
# config value), so the GitHub webhook is off, and GitHub cannot reach a
# laptop's 127.0.0.1 anyway. To add later merges, stop serve, re-run the
# backfill above (repeats are skipped), restart, then re-run step 3's
# `tierd ship` below.
tierd serve --db ~/.tier/tier.db --aggregation team --watch-repo ~/src/your-repo
--aggregation is required and has no default — serve refuses to start
without it, so an existing deployment's privacy posture never changes silently:
developer— subject-bound verified detail on real data, with every named read audited; shared/scoresis name-free.team— team-level aggregates only, k-anonymized, never naming an individual.division— one level higher again.
3. Load past spend. In a second terminal, while serve keeps running:
tierd ship --server http://127.0.0.1:8080 --repo ~/src/your-repo
This step exists because --watch-repo only picks up new activity: it reads
session files written to after serve starts, not the ones already on your
disk. backfill loaded up to 90 days of merged work, so without this step the
score divides that work by only the spend recorded since step 2, and reads too
high: the same work looks cheaper than it was. ship sends the last 90 days of
session files still on your disk.
--serveris the address the step 2 server listens on:http://127.0.0.1:8080, unless you changed it with--addr(§8).--repois the folder of your local checkout, the same folder you passed to--watch-repo.- No
--api-tokenis needed if the server has no token.servetakes its token from theTIER_API_TOKENvariable when--api-tokenis not passed, so if that variable was exported in the step 2 terminal, the server has one: for new installs, import the team map and issue a per-developer upload token on the server withtierd tokens issue --db ~/.tier/tier.db --from-team-map --subject maya --label laptop --upload. Transfer the shown credential securely to a private file on the laptop and useship --developer maya --bound-token @/secure/maya-upload-token. See the identity access runbook for setup. The shared ingest token is a legacy transition credential for existing shippers; keep it configured while migrating them to personal tokens. - A server with no token is safe only on a single-user machine: anyone who can
reach
127.0.0.1:8080can use every route, including erase. If anyone else can reach it, such as other users of a shared machine, or anyone coming through an SSH tunnel or a reverse proxy that forwards the port, set a token (README § The API token). - Running it again is safe: every record carries a key, and the server keeps one copy.
ship reads Claude Code sessions only, unless you add --codex-rollout for
Codex (§6), --opencode for Opencode
(Capturing Opencode; v0.5.2 or later, since v0.4.1
rejects the flag) or --muse for Meta's Muse Code
(Capturing Meta Muse Code; also v0.5.2 or later).
The step 2 serve records the shipped events whether or
not it has that flag, so this step needs no restart. For serve to keep
capturing that tool's new activity on this machine afterwards, stop it with
Ctrl-C and start it again with the same flag added. Without the flag, a
run on a repository where you used only Codex, Opencode or Muse finds nothing
and stops with an error that blames the --repo path.
4. Join your two names. Spend from steps 2 and 3 is recorded under your
computer's login name, which echo $USER prints. Merged work from step 1 is
recorded under your GitHub login, which your GitHub profile page shows, or
which gh api user -q .login prints if you have the GitHub CLI. If the two are
the same, skip this step. If they differ, you appear twice in the scores: once
with spend and no merged work, once with merged work and no spend.
Check before you join them. As the administrator, inspect your computer login with
curl --fail-with-body "http://127.0.0.1:8080/api/v1/developer/$USER/export",
using the admin token as described below if the server has one. This export
works in team/division mode, including before sealing. The sum of cost_micro
in token_events must be above 0, and outcomes must be null or empty (no
merged work). If the export fails, spend is absent, or any outcome is present,
stop here. Merged work may belong to somebody whose GitHub login matches your
computer login. The join moves both spend and merged work, so it would put
that person's work into your score.
If the check passes, tell the server the two names are one person:
curl --fail-with-body -X POST http://127.0.0.1:8080/api/v1/developer_alias \
-H "Content-Type: application/json" \
-d '{"alias":"'"$USER"'","canonical":"<github-login>"}'
The shell fills in your computer login from $USER; replace <github-login>
with your GitHub login. The server answers 201 Created with an empty body, so
the command prints nothing when it works. If the server refuses the join, for
example because one of the names is already part of another join, it prints the
reason. The join also applies to spend and work already loaded. To undo it,
delete it by your computer login, or post it again with the right GitHub login:
curl --fail-with-body -X DELETE "http://127.0.0.1:8080/api/v1/developer_alias/$USER"
None of these commands needs a token header if the server has no token. If it
has one (step 3), send it the way
README § The API token
shows, reading the token from its file (~/.tier/api-token there) so it never
appears on the command line: start the command with
printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" | and add
-H @- to the curl part. The team-server form is in
README § Identity mapping.
Then open http://127.0.0.1:8080, or read the same scores as JSON with
curl -f http://127.0.0.1:8080/api/v1/scores.
The first administrator (root)
For a shared server, set up root before inviting anyone. Root is the top-level
administrator for this server; the API token used by scripts cannot sign in to
the administration pages. Run these commands on the server, as the account
that will run tierd. Stop any existing serve first and use its database path.
mkdir -m 700 -p ~/.tier
[ -s ~/.tier/api-token ] || (umask 077; openssl rand -hex 32 > ~/.tier/api-token)
[ -s ~/.tier/read-token ] || (umask 077; openssl rand -hex 32 > ~/.tier/read-token)
tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
--aggregation team --api-token @$HOME/.tier/api-token \
--read-token @$HOME/.tier/read-token
Wait for the server to start, then press Ctrl-C. This creates a new database if needed: root issuance refuses a missing database. Now issue the first key:
(umask 077; tierd tokens issue --db "$HOME/.tier/tier.db" --root \
--label first-admin > ~/.tier/root-credential.tsv)
The private file contains one tab-separated row; its last field is the
shown-once root key. Save that field in your password manager. Keep the file
private; later lists cannot recover the key. Restart using the same serve
command above. On the server itself, open http://127.0.0.1:8124/signin and
paste only the key. From another machine, first set up HTTPS.
Keep the API token configured on every restart, including loopback: creating
the root key permanently arms organisation authentication for this database.
Build the organisation from the top down
While signed in as root, use these pages in order:
-
Open Organization (
/org). Under Name your levels, useOrg,Division,Teamfor Levels 1–3 and click Save level names. -
Under Add group, create
Acmewith parent Whole company and levelOrg. Then createEngineeringunderAcmeat levelDivision, andPlatformunderEngineeringat levelTeam. Repeat for the other branches. -
Open People (
/people). Add person places a person ID, such asmaya, inPlatform. Use their GitHub login when that is their outcome identity. Give Maya the shown-once claim code through a private channel. They open/claimon this server within 72 hours to make their own personal sign-in key. -
To delegate a branch, first add the manager as a person if they are not already present. Use Invite a manager with their person ID and child group. Give that manager the shown-once node key privately. They can manage that branch and invite managers below it; root can manage all branches. Managing a branch does not grant access to anyone's coaching card.
-
Maya signs in with their personal key, opens My devices (
/devices) and issues a device key for their laptop. They save it in a private file, here/secure/maya-upload-token, and run this on the laptop:tierd ship --server https://tier.example.com --repo ~/src/your-repo \ --developer maya --bound-token @/secure/maya-upload-token
Replace tier.example.com with the HTTPS address configured below. Add the
capture flags from §6 for tools other than Claude Code. Creating people alone
does not satisfy the privacy floor: qualifying uploads must arrive before they
count. Root and managers can still invent people and upload for them, so the
floor does not protect against a malicious administrator.
6. Capturing OpenAI Codex spend
tierd score reads Codex CLI rollouts by default from v0.7.0. Live capture by
serve uses the same reader and is off by default:
tierd serve --aggregation team \
--watch-repo ~/src/your-repo \
--codex-rollout
Two things to know:
--codex-rolloutrequires--watch-repo. Codex spend is attributed to the watched repositories; with no watched repo there is nothing to attribute it to, andservenow refuses to start rather than silently capturing nothing.tierd scorereads Codex by default, alongside Claude Code, Opencode (Z.ai) and Muse Code.serveandship(below) read Codex's own local rollout logs when enabled. The reverse proxy can parse the OpenAI Responses API that Codex speaks (#459), but only for traffic you deliberately point at it with API-key auth, and that path has not been verified against live traffic — the rollout logs are the supported way to capture Codex.
If the server runs centrally and Codex runs on a laptop, the same flag goes on the shipper — it is off by default there too:
https://<your-server>stands for your central server's address, and@/secure/maya-upload-tokenholds Maya's personal upload token, issued on the server. Shared ingest tokens are legacy credentials for existing shippers. Both are explained in the README under What you need before you start.
tierd ship --server https://<your-server> --repo ~/src/your-repo \
--codex-rollout --developer maya --bound-token @/secure/maya-upload-token
Without that flag, a central deployment records the laptop's Codex outcomes (they arrive by webhook, whichever model did the work) while none of its per-developer cost ever lands. That does not read as "Codex is missing" — it reads as "Codex work was free", which inflates the score of exactly the developers moving onto the cheaper path (#492).
Capturing Opencode
Opencode gets the same treatment and the same reasoning, with one extra precondition. It is off by default:
tierd serve --aggregation team \
--watch-repo ~/src/your-repo \
--opencode
--opencode requires v0.5.2 or later; v0.4.1 rejects the flag.
Three things to know:
--opencoderequires--watch-repo, for the same reason--codex-rolloutdoes: with no watched repo there is nothing to attribute the spend to, soserverefuses to start rather than capturing nothing quietly.- GLM-5.3 needs no price-table override. The embedded table prices
glm-5.3andglm-5.3-flashper token at Z.ai's list price (#786). A GLM model with no embedded row, such asglm-5.2, prices at the guessedself-hosted-mediumfallback ($0.50/M), andservelogs a WARN naming it the first time it prices one. - Only routes with an audited rate are captured, and the rest are named. Today that means the Z.ai coding plan and nothing else. Ollama's cloud tier is deliberately excluded because it publishes no per-token rate; capturing it would price the spend at that same guessed fallback and inject invented dollars into a cost-per-outcome metric. The exclusion, its reason, and its per-scan row count are logged, so what is left out is visible rather than inferred.
The shipper mirrors it, off by default there too:
tierd ship --server https://<your-server> --repo ~/src/your-repo \
--opencode --api-token @$HOME/.tier/ingest-token
⚠️ Do not also route Opencode through TIER's reverse proxy. Its spend would be
captured twice and the two rows cannot dedup — this collector keys on Opencode's
local message.id, the proxy keys on the upstream response id, and neither side
can compute the other's value, so the rows add rather than colliding. serve
warns at startup when both are enabled.
⚠️ Pass --repo explicitly on ship. It defaults to ., and a cron or
launchd job usually runs from $HOME. If $HOME is not a git checkout the run
fails loudly — exit 1, "does not appear to be a git repository". And if $HOME
is a checkout, such as a dotfiles repo, every session belonging to another
repository falls outside the scope and nothing ships — which as of #549 is also
exit 1, with a per-repo summary naming the zero rows:
Per-repo summary:
"/Users/you": sessions_with_events=0 events_shipped=0
ship: every --repo target kept 0 sessions since 2026-05-05 — this is almost
always a wrong --repo path, not a legitimately idle repo. Pass --allow-empty if
zero is genuinely expected.
That case used to exit 0 silently, so "ship reported success" and "ship recovered
nothing" were indistinguishable without diffing the store by hand. If a repo is
legitimately idle for the whole window, pass --allow-empty (env
TIER_SHIP_ALLOW_EMPTY) to get exit 0 back.
The guard measures the whole run, not just Claude Code: a machine that only produces Codex spend exits 0 normally, because the Codex events count even though they are scanned once across all repos rather than per repo.
Capturing Meta Muse Code
Muse Code is Meta's coding agent, run as the muse command. TIER reads the
session log Muse writes for each session, and it is off by default:
tierd serve --aggregation team \
--watch-repo ~/src/your-repo \
--muse
--muse requires v0.5.2 or later; v0.4.1 rejects the flag.
Where the input comes from: Muse writes one log per session to
~/.local/share/muse/sessions/YYYY/MM/DD/<session-id>/session.jsonl, with no
setup on your part. ls ~/.local/share/muse/sessions shows whether this machine
has any. If your Muse data lives elsewhere, set home: in the collectors.muse
block of the config file (config.example.yaml shows it) or pass --muse-home
to ship; TIER reads the sessions folder inside it.
Five things to know:
--muserequires--watch-repo, as--codex-rolloutdoes. A Muse session run in a folder outside every watched repository is skipped, not attributed.- TIER decodes counts, never text. From each session log it decodes token counts, model names, the session id (which it stores), each run's recorded branch, the folder Muse ran in (used to pick the repository, then dropped) and the ids linking a subagent to the run that started it. It never decodes your prompts, Muse's replies or tool output (privacy.md).
- A run is recorded after it finishes. Muse writes a run's git branch only
when the run ends, so TIER waits for that before recording the run's calls:
expect a run's cost to appear after the run completes, not while it works. If
a run's session log goes 6 hours untouched with no branch recorded, TIER
records that run as
unattributed:detached-head. That includes a run still waiting on you at a tool-approval prompt after 6 hours, and TIER cannot fix its issue afterwards. - Subagent spend counts toward the run that started it. Calls made by a Muse subagent are recorded under the repository and branch of the run that started it, once that run has finished. Three kinds are still left out, each with a warning in the log: a subagent that Muse's log never links to one run (TIER waits until neither log has changed for 6 hours, then leaves it out rather than guess), a subagent started by another subagent, and a subagent whose parent session recorded no folder. For those, TIER's Muse figure is lower than what Muse actually spent.
- Muse is priced at Meta's published per-token API rates, even if you pay
for Muse by subscription (see
reference-price-table.md).
tierd scorereads Muse by default;serveandshipread it when enabled.
To confirm it works, look for these lines in the serve output:
Muse collector enabled with the sessions_dir it is reading, then
muse scan complete with an events= count (subagent_events= is the part
that came from subagents). That line is written only for a scan that had a
session log to read; a scan with none writes nothing, so a quiet log on an idle
machine is normal. If the sessions folder does not exist, it logs
muse sessions root does not exist; nothing to scan instead.
tier_muse_events_total at http://127.0.0.1:8080/metrics counts events handed
to the store, repeats included: a session log TIER reads again (a changed one, one
still waiting on a run, or every one after a restart) re-sends its calls, and the
store keeps one copy. A rising value shows capture is alive; it is not a count of
stored rows. If the server has a token, send it as
Authorization: Bearer <token>; in developer mode the read-only token
(--read-token) is enough for /metrics, and in team or division mode use
the metrics token (--metrics-token) instead. Read it from its file so it stays
off the command line, as in step 4 of §5:
printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
curl -sf -H @- http://127.0.0.1:8080/metrics | grep tier_muse_events_total
Use your metrics token's file instead if you have one (in developer mode, the
read token's file also works). With no token, drop the
printf … | line and -H @-. tierd doctor does not check Muse.
The shipper mirrors it, off by default there too. https://<your-server> and
@$HOME/.tier/ingest-token mean what the note in §6 says:
tierd ship --server https://<your-server> --repo ~/src/your-repo \
--muse --api-token @$HOME/.tier/ingest-token
Its summary prints a muse (all repos): events_shipped=<n> line. A run still
in progress is not shipped; the next ship picks it up once it finishes.
7. Viewing the dashboard from another machine
The demo is safe to expose directly — it is read-only and every row is synthetic:
tierd demo --addr 0.0.0.0:8124 # then http://<this-host>:8124
HTTPS with Caddy
TIER serves plain HTTP only. Signing in from another machine requires HTTPS. Put a reverse proxy in front: it accepts the browser's encrypted HTTPS connection, presents a certificate proving the server's name, and forwards the request to TIER over HTTP on the same machine. Caddy, nginx or your load balancer can do this. The example below uses Caddy and a public DNS name you control.
-
Set up the first administrator above. Point your DNS name (here
tier.example.com; replace it throughout) at this host. Allow inbound TCP ports 80 and 443 to Caddy; leave TIER's port 8124 bound to loopback. Caddy needs those public ports to obtain its certificate in this example. -
Install Caddy on that same host. Stop
serveand restart it trusting only this local proxy:tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \ --aggregation team --api-token @$HOME/.tier/api-token \ --read-token @$HOME/.tier/read-token --trusted-proxy-cidr 127.0.0.1/32Keep any capture flags from your existing server command. The trusted proxy setting lets TIER accept Caddy's HTTPS scheme and client address headers. Do not trust arbitrary client networks.
-
Save this as
/etc/caddy/Caddyfile, replacing the hostname:tier.example.com { reverse_proxy 127.0.0.1:8124 }With Caddy's standard Linux systemd installation, load it with:
sudo systemctl reload caddy -
Open
https://tier.example.com/signinfrom the other machine and use the root key. Caddy obtains and renews the certificate and redirects HTTP to HTTPS. See its reverse proxy guide.
Give administrators their scoped node keys and developers their personal keys,
through a password manager or another private channel. A shared read-only token
still works in the existing dashboard's token box for name-free reports; it
cannot administer the organisation. Keep the API token for host operations and
scripts, which send it as Authorization: Bearer. The API and read-token files
must contain different values. See
What you need before you start.
Private certificate authorities: SSL_CERT_FILE
A certificate authority (CA) signs certificates that clients trust. Public CAs normally work without extra setup. If your proxy uses a private company CA, install its CA certificate in each browser machine's trust store. A TIER process making HTTPS requests must trust it too: shippers on laptops, the child sending to a central server, and the company server pulling an import from a source.
Ask your administrator for a PEM CA bundle, then set this in the environment before starting the process that connects:
export SSL_CERT_FILE=/absolute/path/company-ca-bundle.pem
tierd ship --server https://tier.example.com --repo ~/src/your-repo \
--developer maya --bound-token @/secure/maya-upload-token
For federation or import, set it in the service environment and restart serve.
This is Go's certificate trust setting,
not a tierd flag; it changes outbound trust and does not enable HTTPS serving.
It overrides the default certificate-file location, so use a bundle containing
the public roots you still need as well as the private CA. It does not install
trust in your browser. Keep certificate verification enabled.
Publishing the first month
Team and division modes show closed, sealed calendar months only. They do
not show live spend. This preserves the privacy floor: overlapping date windows
could be subtracted to isolate one person's figures even when each group clears
k-anonymity. Team/division mode withholds numbers when fewer than k contributing
developers are present (default k = 5, minimum 3). Each person can use tierd score --outcomes for their own local TIER,
tierd score for the Claude Code, Codex CLI, Opencode (Z.ai) and Muse Code cost table and issue-attributed spend share,
and tierd demo for the dashboard on invented data. Sealing does not remove
that floor. From v0.6.1 through v0.6.3, real-data developer mode is refused;
from v0.7.0, it requires subject-bound credentials and a named audited operator.
Before the first seal, GET /api/v1/scores returns 404 with this error:
sealing not armed: no month is sealed until the operator arms sealing with seal_from or `tierd seal --arm` (#913)
The dashboard shows YYYY-MM is not published: followed by that reason, with
no scores. Opening or refreshing it never creates a seal.
On the server, finish backfilling cost and outcomes and loading the team and
alias maps before arming: a sealed month never changes, and the first seal
permanently pins the earliest published month. Leave serve running so it has
registered its sources. In another terminal, under the same account and with
the same environment, preview and then confirm the first seal:
tierd seal requires v0.5.2 or newer, so these commands are an exception
to the released-binary check at the top of this page.
tierd seal --arm earliest --dry-run --aggregation team --db "$HOME/.tier/tier.db"
tierd seal --arm earliest --aggregation team --db "$HOME/.tier/tier.db"
tierd seal --status --aggregation team --db "$HOME/.tier/tier.db" \
--server http://127.0.0.1:8124 --api-token @$HOME/.tier/read-token
The second command prompts for confirmation, seals the first eligible month,
and arms future sealing. earliest selects the first full UTC month of cost
coverage; use --arm YYYY-MM to start later. Arming is possible only once that
full month has closed plus --report-grace (default 336h, 14 days), with
configured sources settled through its end. For example, cost data starting
2026-09-03 makes October the first full UTC month: it closes at 2026-11-01
00:00Z and is armable from 2026-11-15 00:00Z with the default grace.
With no eligible history yet, the command refuses: backfill the missing history
or wait until a full covered month and its grace have elapsed. It also refuses
a month whose start predates the retention horizon. If you customized serve,
also pass its same --config, --prices,
--k-anonymity, --report-grace and database settings to seal.
After success, a read with no month (GET /api/v1/scores) returns the newest
sealed month, so the armed month shows immediately. To select the month printed
by seal explicitly, use GET /api/v1/scores?period=YYYY-MM. The writable server
seals subsequent eligible months at startup and on the next hourly pass
(within the hour, once sources are settled); refresh the dashboard to see them.
See the sealed-read contract
for unavailable months and source holds.
If you would rather not open a port at all, tunnel over SSH from the machine you are browsing on — nothing is exposed to the network:
ssh -N -L 8080:127.0.0.1:8080 you@the-host # then http://127.0.0.1:8080 locally
Use 8080:127.0.0.1:8124 instead for the server above. The browser then connects
to local loopback, where HTTP sign-in is allowed. This tunnel does not give
federation or company import an HTTPS endpoint.
Link a second server and send totals
Federation lets a child server keep its people and data locally while sending name-free group totals to a central server. Set up root and HTTPS on both first.
-
On the central server, sign in as root and open Federation (
/federation). Under Allow a federated server, enter the child's name and hostname, choose its parent group and report schedule, then click Allow server and issue token. Save the shown-once federation token privately. To get its issuedserver_id, run on the central host:printf 'Authorization: Bearer %s\n' "$(cut -f4 ~/.tier/root-credential.tsv)" | curl --fail-with-body -H @- http://127.0.0.1:8124/api/v1/org/federationFind the child's entry by label and host in the returned JSON. Copy its
server_id, not the group ID or credential ID. -
On the child host, make a private directory owned by the
tierdaccount:mkdir -m 700 -p ~/.tier/federationStop its
serve, then restart with the three host settings below. ReplaceCENTRAL_ISSUED_SERVER_IDwith that ID and the URL with your central origin:tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \ --aggregation team --api-token @$HOME/.tier/api-token \ --read-token @$HOME/.tier/read-token --trusted-proxy-cidr 127.0.0.1/32 \ --federation-central-url https://central.example.com \ --federation-token-file "$HOME/.tier/federation/token" \ --federation-server-id CENTRAL_ISSUED_SERVER_ID -
Sign in as the child's root, open Child server settings (
/upstream), paste the federation token, choose the schedule/hour in UTC and click Save. This writes the token to the host file with mode0600; it is never shown again. Test connection checks authentication only and sends no totals. Saving the schedule does not send a report either. -
Finish loading history and arm sealing on the child using the steps above. The scheduled sender sends eligible closed, sealed months, including withheld months with explicit withholding and zero wire figures. Those zeros represent suppressed figures, not measured zero activity. Unavailable node months, including unsealed months, are omitted. Check Last successful report on the child and Last authenticated report on the central Federation page.
Central totals are reported by the child, not re-checked centrally. Revoking its federation token stops future reports and keeps already accepted history.
A one-time company import
Use Company import to bring an existing installation's history into the company server once. Federation is the ongoing totals link. Use HTTPS at the source and company, and check private-CA trust before consuming an export key.
- On the source, each person signs in with their own personal key, opens
Source export (
/import-source), chooses Move my named data, enters the destination label and saves if they consent. Without consent, only eligible name-free totals move. Root cannot choose consent for them. - Source root opens the same page and clicks Issue one-time export key. Transfer it privately to company root. It expires after one hour and the preview pull consumes it; another pull needs a new key.
- Company root opens Company import (
/import), enters the source's HTTPS address, selects the destination group, pastes the export key and clicks Preview import. Resolve every identity clash explicitly and save the mappings; matching IDs never merge automatically. Review the counts, omitted rows and live-upload warning before clicking Import. - Open People on the company server. Issue claim codes for imported people; they claim personal keys and create device keys here as above. Source keys and viewer declarations do not move. Imported people do not count toward the privacy floor until qualifying uploads arrive here.
Arrange for each developer to upload to one server going forward. An import does not erase the source; a later erasure of named data must be requested at both servers. See the import contract for the preview and apply limits.
8. Running on a different port
Every server command takes --addr host:port:
tierd demo --addr 127.0.0.1:8123
tierd serve --addr 127.0.0.1:9000 --aggregation team --watch-repo ~/src/your-repo
9. What each command actually needs
| Command | Needs a repo? | Needs a token? | Produces |
|---|---|---|---|
tierd demo |
no | no | synthetic dashboard |
tierd doctor |
yes (--repo) |
no | a capture health check |
tierd score |
yes (--repo) |
only with --outcomes |
cost per issue from Claude Code, Codex CLI, Opencode (Z.ai), Muse Code; opt-in personal TIER |
tierd backfill |
yes (--repo owner/name) |
GitHub token (how) | reconstructed outcomes |
tierd serve |
for live capture (--watch-repo) |
for non-loopback binds | the full TIER score + dashboard |
Run any command with -h for its full flag list.